From ab4c4fde1a3ec846132b2003bfc3ed3792985037 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 17 Nov 2025 16:54:59 +0200 Subject: [PATCH 01/32] fix(document): fix types for document deletion --- src/typesense/types/document.py | 27 +++++++++++++++++++++++---- tests/documents_test.py | 11 +++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/typesense/types/document.py b/src/typesense/types/document.py index a0b63b4..496432d 100644 --- a/src/typesense/types/document.py +++ b/src/typesense/types/document.py @@ -915,23 +915,42 @@ class DeleteSingleDocumentParameters(typing.TypedDict): ignore_not_found: typing.NotRequired[bool] -class DeleteQueryParameters(typing.TypedDict): +class TruncateDeleteParameters(typing.TypedDict): """ - Parameters for deleting documents. + Parameters for truncating a collection (deleting all documents, keeping schema). + + Attributes: + truncate (bool): Truncate the collection, keeping just the schema. + """ + + truncate: bool + + +class FilterDeleteParameters(typing.TypedDict): + """ + Parameters for deleting documents by filter. Attributes: - truncate (str): Truncate the collection, keeping just the schema. filter_by (str): Filter to apply to documents. batch_size (int): Batch size for deleting documents. ignore_not_found (bool): Ignore not found documents. """ - truncate: typing.NotRequired[bool] filter_by: str batch_size: typing.NotRequired[int] ignore_not_found: typing.NotRequired[bool] +DeleteQueryParameters = typing.Union[TruncateDeleteParameters, FilterDeleteParameters] +""" +Discriminated union of parameters for deleting documents. + +Either: + - TruncateDeleteParameters: Use truncate to delete all documents, keeping the schema. + - FilterDeleteParameters: Use filter_by (and optionally batch_size, ignore_not_found) to delete specific documents. +""" + + class DeleteResponse(typing.TypedDict): """ Response from deleting documents. diff --git a/tests/documents_test.py b/tests/documents_test.py index 9926798..994845a 100644 --- a/tests/documents_test.py +++ b/tests/documents_test.py @@ -206,6 +206,17 @@ def test_delete( assert response == {"num_deleted": 1} +def test_truncate( + actual_documents: Documents[Companies], + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the Documents object can delete a document from Typesense server.""" + response = actual_documents.delete({"truncate": True}) + assert response == {"num_deleted": 1} + + def test_delete_ignore_missing( actual_documents: Documents[Companies], delete_all: None, From 175af19ac108458116f3bd76630b56e89e0ec25e Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Thu, 4 Dec 2025 15:51:47 +0200 Subject: [PATCH 02/32] chore: bump mypy --- pyproject.toml | 2 +- src/typesense/analytics_rule_v1.py | 2 +- src/typesense/analytics_rules_v1.py | 2 +- src/typesense/override.py | 2 +- src/typesense/overrides.py | 4 +- src/typesense/synonym.py | 2 +- src/typesense/synonyms.py | 2 +- uv.lock | 168 ++++++++++++++++++++++------ 8 files changed, 141 insertions(+), 43 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 59537c5..e363807 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ build-backend = "setuptools.build_meta" [dependency-groups] dev = [ - "mypy", + "mypy>=1.19.0", "pytest", "coverage", "pytest-mock", diff --git a/src/typesense/analytics_rule_v1.py b/src/typesense/analytics_rule_v1.py index 87a156d..ff9faa6 100644 --- a/src/typesense/analytics_rule_v1.py +++ b/src/typesense/analytics_rule_v1.py @@ -53,7 +53,7 @@ class AnalyticsRuleV1: rule_id (str): The ID of the analytics rule. """ - @warn_deprecation( # type: ignore[misc] + @warn_deprecation( # type: ignore[untyped-decorator] "AnalyticsRuleV1 is deprecated on v30+. Use client.analytics.rules[rule_id] instead.", flag_name="analytics_rules_v1_deprecation", ) diff --git a/src/typesense/analytics_rules_v1.py b/src/typesense/analytics_rules_v1.py index 2c93a98..c2cf1eb 100644 --- a/src/typesense/analytics_rules_v1.py +++ b/src/typesense/analytics_rules_v1.py @@ -65,7 +65,7 @@ class AnalyticsRulesV1(object): resource_path: typing.Final[str] = "/analytics/rules" - @warn_deprecation( # type: ignore[misc] + @warn_deprecation( # type: ignore[untyped-decorator] "AnalyticsRulesV1 is deprecated on v30+. Use client.analytics instead.", flag_name="analytics_rules_v1_deprecation", ) diff --git a/src/typesense/override.py b/src/typesense/override.py index a9613b0..099d283 100644 --- a/src/typesense/override.py +++ b/src/typesense/override.py @@ -42,7 +42,7 @@ class Override: override_id (str): The ID of the override. """ - @warn_deprecation( # type: ignore[misc] + @warn_deprecation( # type: ignore[untyped-decorator] "The override API (collections/{collection}/overrides/{override_id}) is deprecated is removed on v30+. " "Use curation sets (curation_sets) instead.", flag_name="overrides_deprecation", diff --git a/src/typesense/overrides.py b/src/typesense/overrides.py index 8581e93..696c133 100644 --- a/src/typesense/overrides.py +++ b/src/typesense/overrides.py @@ -25,8 +25,6 @@ versions through the use of the typing_extensions library. """ -from __future__ import annotations - import sys from typing_extensions import deprecated @@ -63,7 +61,7 @@ class Overrides: resource_path: typing.Final[str] = "overrides" - @warn_deprecation( # type: ignore[misc] + @warn_deprecation( # type: ignore[untyped-decorator] "Overrides is deprecated on v30+. Use client.curation_sets instead.", flag_name="overrides_deprecation", ) diff --git a/src/typesense/synonym.py b/src/typesense/synonym.py index 6bea97d..020b8d4 100644 --- a/src/typesense/synonym.py +++ b/src/typesense/synonym.py @@ -39,7 +39,7 @@ class Synonym: synonym_id (str): The ID of the synonym. """ - @warn_deprecation( # type: ignore[misc] + @warn_deprecation( # type: ignore[untyped-decorator] "The synonym API (collections/{collection}/synonyms/{synonym_id}) is deprecated is removed on v30+. " "Use synonym sets (synonym_sets) instead.", flag_name="synonyms_deprecation", diff --git a/src/typesense/synonyms.py b/src/typesense/synonyms.py index fe5f508..543bedd 100644 --- a/src/typesense/synonyms.py +++ b/src/typesense/synonyms.py @@ -61,7 +61,7 @@ class Synonyms: resource_path: typing.Final[str] = "synonyms" - @warn_deprecation( # type: ignore[misc] + @warn_deprecation( # type: ignore[untyped-decorator] "The synonyms API (collections/{collection}/synonyms) is deprecated is removed on v30+. " "Use synonym sets (synonym_sets) instead.", flag_name="synonyms_deprecation", diff --git a/uv.lock b/uv.lock index 376f24b..994eaa3 100644 --- a/uv.lock +++ b/uv.lock @@ -223,48 +223,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, ] +[[package]] +name = "librt" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/c3/cdff3c10e2e608490dc0a310ccf11ba777b3943ad4fcead2a2ade98c21e1/librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae", size = 54209, upload-time = "2025-11-29T14:01:56.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/84/859df8db21dedab2538ddfbe1d486dda3eb66a98c6ad7ba754a99e25e45e/librt-0.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1", size = 27294, upload-time = "2025-11-29T14:00:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/f7/01/ec3971cf9c4f827f17de6729bdfdbf01a67493147334f4ef8fac68936e3a/librt-0.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf", size = 27635, upload-time = "2025-11-29T14:00:36.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f9/3efe201df84dd26388d2e0afa4c4dc668c8e406a3da7b7319152faf835a1/librt-0.6.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c5b31bed2c2f2fa1fcb4815b75f931121ae210dc89a3d607fb1725f5907f1437", size = 81768, upload-time = "2025-11-29T14:00:37.451Z" }, + { url = "https://files.pythonhosted.org/packages/0a/13/f63e60bc219b17f3d8f3d13423cd4972e597b0321c51cac7bfbdd5e1f7b9/librt-0.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f8ed5053ef9fb08d34f1fd80ff093ccbd1f67f147633a84cf4a7d9b09c0f089", size = 85884, upload-time = "2025-11-29T14:00:38.433Z" }, + { url = "https://files.pythonhosted.org/packages/c2/42/0068f14f39a79d1ce8a19d4988dd07371df1d0a7d3395fbdc8a25b1c9437/librt-0.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f0e4bd9bcb0ee34fa3dbedb05570da50b285f49e52c07a241da967840432513", size = 85830, upload-time = "2025-11-29T14:00:39.418Z" }, + { url = "https://files.pythonhosted.org/packages/14/1c/87f5af3a9e6564f09e50c72f82fc3057fd42d1facc8b510a707d0438c4ad/librt-0.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f89c8d20dfa648a3f0a56861946eb00e5b00d6b00eea14bc5532b2fcfa8ef1", size = 88086, upload-time = "2025-11-29T14:00:40.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/22153b98b88a913b5b3f266f12e57df50a2a6960b3f8fcb825b1a0cfe40a/librt-0.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecc2c526547eacd20cb9fbba19a5268611dbc70c346499656d6cf30fae328977", size = 86470, upload-time = "2025-11-29T14:00:41.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/ea1edb587799b1edcc22444e0630fa422e32d7aaa5bfb5115b948acc2d1c/librt-0.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fbedeb9b48614d662822ee514567d2d49a8012037fc7b4cd63f282642c2f4b7d", size = 89079, upload-time = "2025-11-29T14:00:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/73/ad/50bb4ae6b07c9f3ab19653e0830a210533b30eb9a18d515efb5a2b9d0c7c/librt-0.6.3-cp310-cp310-win32.whl", hash = "sha256:0765b0fe0927d189ee14b087cd595ae636bef04992e03fe6dfdaa383866c8a46", size = 19820, upload-time = "2025-11-29T14:00:44.211Z" }, + { url = "https://files.pythonhosted.org/packages/7a/12/7426ee78f3b1dbe11a90619d54cb241ca924ca3c0ff9ade3992178e9b440/librt-0.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c659f9fb8a2f16dc4131b803fa0144c1dadcb3ab24bb7914d01a6da58ae2457", size = 21332, upload-time = "2025-11-29T14:00:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/8b/80/bc60fd16fe24910bf5974fb914778a2e8540cef55385ab2cb04a0dfe42c4/librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08", size = 27285, upload-time = "2025-11-29T14:00:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/26335536ed9ba097c79cffcee148393592e55758fe76d99015af3e47a6d0/librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa", size = 27629, upload-time = "2025-11-29T14:00:47.863Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/2dcedeacfedee5d2eda23e7a49c1c12ce6221b5d58a13555f053203faafc/librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122", size = 82039, upload-time = "2025-11-29T14:00:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/48/ff/6aa11914b83b0dc2d489f7636942a8e3322650d0dba840db9a1b455f3caa/librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f", size = 86560, upload-time = "2025-11-29T14:00:50.403Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/d25af61958c2c7eb978164aeba0350719f615179ba3f428b682b9a5fdace/librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040", size = 86494, upload-time = "2025-11-29T14:00:51.383Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/40e75d3b258c801908e64b39788f9491635f9554f8717430a491385bd6f2/librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523", size = 88914, upload-time = "2025-11-29T14:00:52.688Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/0070c81aba8a169224301c75fb5fb6c3c25ca67e6ced086584fc130d5a67/librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f", size = 86944, upload-time = "2025-11-29T14:00:53.768Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/809f38887941b7726692e0b5a083dbdc87dbb8cf893e3b286550c5f0b129/librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47", size = 89852, upload-time = "2025-11-29T14:00:54.71Z" }, + { url = "https://files.pythonhosted.org/packages/58/a3/b0e5b1cda675b91f1111d8ba941da455d8bfaa22f4d2d8963ba96ccb5b12/librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7", size = 19948, upload-time = "2025-11-29T14:00:55.989Z" }, + { url = "https://files.pythonhosted.org/packages/cc/73/70011c2b37e3be3ece3affd3abc8ebe5cda482b03fd6b3397906321a901e/librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8", size = 21406, upload-time = "2025-11-29T14:00:56.874Z" }, + { url = "https://files.pythonhosted.org/packages/91/ee/119aa759290af6ca0729edf513ca390c1afbeae60f3ecae9b9d56f25a8a9/librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45", size = 20875, upload-time = "2025-11-29T14:00:57.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2c/b59249c566f98fe90e178baf59e83f628d6c38fb8bc78319301fccda0b5e/librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176", size = 27841, upload-time = "2025-11-29T14:00:58.925Z" }, + { url = "https://files.pythonhosted.org/packages/40/e8/9db01cafcd1a2872b76114c858f81cc29ce7ad606bc102020d6dabf470fb/librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057", size = 27844, upload-time = "2025-11-29T14:01:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/59/4d/da449d3a7d83cc853af539dee42adc37b755d7eea4ad3880bacfd84b651d/librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610", size = 84091, upload-time = "2025-11-29T14:01:01.118Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6c/f90306906fb6cc6eaf4725870f0347115de05431e1f96d35114392d31fda/librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c", size = 88239, upload-time = "2025-11-29T14:01:02.11Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ae/473ce7b423cfac2cb503851a89d9d2195bf615f534d5912bf86feeebbee7/librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9", size = 88815, upload-time = "2025-11-29T14:01:03.114Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6d/934df738c87fb9617cabefe4891eece585a06abe6def25b4bca3b174429d/librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c", size = 90598, upload-time = "2025-11-29T14:01:04.071Z" }, + { url = "https://files.pythonhosted.org/packages/72/89/eeaa124f5e0f431c2b39119550378ae817a4b1a3c93fd7122f0639336fff/librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c", size = 88603, upload-time = "2025-11-29T14:01:05.02Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ed/c60b3c1cfc27d709bc0288af428ce58543fcb5053cf3eadbc773c24257f5/librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf", size = 92112, upload-time = "2025-11-29T14:01:06.304Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/f56169be5f716ef4ab0277be70bcb1874b4effc262e655d85b505af4884d/librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d", size = 20127, upload-time = "2025-11-29T14:01:07.283Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/222750ce82bf95125529eaab585ac7e2829df252f3cfc05d68792fb1dd2c/librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01", size = 21545, upload-time = "2025-11-29T14:01:08.184Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/f731ddcfb72f446a92a8674c6b8e1e2242773cce43a04f41549bd8b958ff/librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55", size = 20946, upload-time = "2025-11-29T14:01:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/3055dd440f8b8b3b7e8624539a0749dd8e1913e978993bcca9ce7e306231/librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3", size = 27874, upload-time = "2025-11-29T14:01:10.615Z" }, + { url = "https://files.pythonhosted.org/packages/ef/93/226d7dd455eaa4c26712b5ccb2dfcca12831baa7f898c8ffd3a831e29fda/librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5", size = 27852, upload-time = "2025-11-29T14:01:11.535Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8b/db9d51191aef4e4cc06285250affe0bb0ad8b2ed815f7ca77951655e6f02/librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42", size = 84264, upload-time = "2025-11-29T14:01:12.461Z" }, + { url = "https://files.pythonhosted.org/packages/8d/53/297c96bda3b5a73bdaf748f1e3ae757edd29a0a41a956b9c10379f193417/librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70", size = 88432, upload-time = "2025-11-29T14:01:13.405Z" }, + { url = "https://files.pythonhosted.org/packages/54/3a/c005516071123278e340f22de72fa53d51e259d49215295c212da16c4dc2/librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522", size = 89014, upload-time = "2025-11-29T14:01:14.373Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9b/ea715f818d926d17b94c80a12d81a79e95c44f52848e61e8ca1ff29bb9a9/librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30", size = 90807, upload-time = "2025-11-29T14:01:15.377Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fc/4e2e4c87e002fa60917a8e474fd13c4bac9a759df82be3778573bb1ab954/librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917", size = 88890, upload-time = "2025-11-29T14:01:16.633Z" }, + { url = "https://files.pythonhosted.org/packages/70/7f/c7428734fbdfd4db3d5b9237fc3a857880b2ace66492836f6529fef25d92/librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530", size = 92300, upload-time = "2025-11-29T14:01:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0c/738c4824fdfe74dc0f95d5e90ef9e759d4ecf7fd5ba964d54a7703322251/librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3", size = 20159, upload-time = "2025-11-29T14:01:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/f2/95/93d0e61bc617306ecf4c54636b5cbde4947d872563565c4abdd9d07a39d3/librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88", size = 21484, upload-time = "2025-11-29T14:01:19.506Z" }, + { url = "https://files.pythonhosted.org/packages/10/23/abd7ace79ab54d1dbee265f13529266f686a7ce2d21ab59a992f989009b6/librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9", size = 20935, upload-time = "2025-11-29T14:01:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/83/14/c06cb31152182798ed98be73f54932ab984894f5a8fccf9b73130897a938/librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a", size = 27566, upload-time = "2025-11-29T14:01:21.609Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/ce83ca7b057b06150519152f53a0b302d7c33c8692ce2f01f669b5a819d9/librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd", size = 27753, upload-time = "2025-11-29T14:01:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ec/739a885ef0a2839b6c25f1b01c99149d2cb6a34e933ffc8c051fcd22012e/librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b", size = 83178, upload-time = "2025-11-29T14:01:23.555Z" }, + { url = "https://files.pythonhosted.org/packages/db/bd/dc18bb1489d48c0911b9f4d72eae2d304ea264e215ba80f1e6ba4a9fc41d/librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc", size = 87266, upload-time = "2025-11-29T14:01:24.532Z" }, + { url = "https://files.pythonhosted.org/packages/94/f3/d0c5431b39eef15e48088b2d739ad84b17c2f1a22c0345c6d4c4a42b135e/librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3", size = 87623, upload-time = "2025-11-29T14:01:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/9a52e90834e4bd6ee16cdbaf551cb32227cbaad27398391a189c489318bc/librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751", size = 89436, upload-time = "2025-11-29T14:01:26.769Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8a/a7e78e46e8486e023c50f21758930ef4793999115229afd65de69e94c9cc/librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8", size = 87540, upload-time = "2025-11-29T14:01:27.756Z" }, + { url = "https://files.pythonhosted.org/packages/49/01/93799044a1cccac31f1074b07c583e181829d240539657e7f305ae63ae2a/librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e", size = 90597, upload-time = "2025-11-29T14:01:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/a7/29/00c7f58b8f8eb1bad6529ffb6c9cdcc0890a27dac59ecda04f817ead5277/librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf", size = 18955, upload-time = "2025-11-29T14:01:30.325Z" }, + { url = "https://files.pythonhosted.org/packages/d7/13/2739e6e197a9f751375a37908a6a5b0bff637b81338497a1bcb5817394da/librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2", size = 20263, upload-time = "2025-11-29T14:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/e1/73/393868fc2158705ea003114a24e73bb10b03bda31e9ad7b5c5ec6575338b/librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78", size = 19575, upload-time = "2025-11-29T14:01:32.229Z" }, + { url = "https://files.pythonhosted.org/packages/48/6d/3c8ff3dec21bf804a205286dd63fd28dcdbe00b8dd7eb7ccf2e21a40a0b0/librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179", size = 28732, upload-time = "2025-11-29T14:01:33.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/e214b8b4aa34ed3d3f1040719c06c4d22472c40c5ef81a922d5af7876eb4/librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7", size = 29065, upload-time = "2025-11-29T14:01:34.088Z" }, + { url = "https://files.pythonhosted.org/packages/ab/90/ef61ed51f0a7770cc703422d907a757bbd8811ce820c333d3db2fd13542a/librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916", size = 93703, upload-time = "2025-11-29T14:01:35.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/c30bb119c35962cbe9a908a71da99c168056fc3f6e9bbcbc157d0b724d89/librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8", size = 98890, upload-time = "2025-11-29T14:01:36.031Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/47a4a78d252d36f072b79d592df10600d379a895c3880c8cbd2ac699f0ad/librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc", size = 98255, upload-time = "2025-11-29T14:01:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/e5/28/779b5cc3cd9987683884eb5f5672e3251676bebaaae6b7da1cf366eb1da1/librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07", size = 100769, upload-time = "2025-11-29T14:01:38.413Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/771755e57c375cb9d25a4e106f570607fd856e2cb91b02418db1db954796/librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f", size = 98580, upload-time = "2025-11-29T14:01:39.459Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ec/8b157eb8fbc066339a2f34b0aceb2028097d0ed6150a52e23284a311eafe/librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0", size = 101706, upload-time = "2025-11-29T14:01:40.474Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/4aaead9a06c795a318282aebf7d3e3e578fa889ff396e1b640c3be4c7806/librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a", size = 19465, upload-time = "2025-11-29T14:01:41.77Z" }, + { url = "https://files.pythonhosted.org/packages/3a/61/b7e6a02746c1731670c19ba07d86da90b1ae45d29e405c0b5615abf97cde/librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf", size = 21042, upload-time = "2025-11-29T14:01:42.865Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246, upload-time = "2025-11-29T14:01:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/d0/85/63b34f02f56b86574bb6d1ed29415346332a1edea2cd12b29fb0863456cb/librt-0.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09262cb2445b6f15d09141af20b95bb7030c6f13b00e876ad8fdd1a9045d6aa5", size = 27438, upload-time = "2025-11-29T14:01:45.101Z" }, + { url = "https://files.pythonhosted.org/packages/57/cc/d2952a97b5e19c0a88f04b161d1c4b8336ad093a8fecd49801258b3cc816/librt-0.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57705e8eec76c5b77130d729c0f70190a9773366c555c5457c51eace80afd873", size = 27783, upload-time = "2025-11-29T14:01:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/09/38/007124092f9345a273b27b070e894afa66a737b576f1f7c37354dd4ffe24/librt-0.6.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3ac2a7835434b31def8ed5355dd9b895bbf41642d61967522646d1d8b9681106", size = 81403, upload-time = "2025-11-29T14:01:47.659Z" }, + { url = "https://files.pythonhosted.org/packages/89/ca/079722b2b518bc6c38e3ba7ab07f2f0e5c6d4905d7f34ee138f862a69853/librt-0.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71f0a5918aebbea1e7db2179a8fe87e8a8732340d9e8b8107401fb407eda446e", size = 85678, upload-time = "2025-11-29T14:01:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7a/dcba5bd2f71d3815ea3886929f218c259665b9f2f7115163eed2ccb50599/librt-0.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa346e202e6e1ebc01fe1c69509cffe486425884b96cb9ce155c99da1ecbe0e9", size = 85655, upload-time = "2025-11-29T14:01:49.894Z" }, + { url = "https://files.pythonhosted.org/packages/7a/47/5bfa3816b58e6105d1ea1a165af73ae6560b4acb80ce3304793ac9a36ec1/librt-0.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92267f865c7bbd12327a0d394666948b9bf4b51308b52947c0cc453bfa812f5d", size = 88093, upload-time = "2025-11-29T14:01:50.902Z" }, + { url = "https://files.pythonhosted.org/packages/76/33/ac5c01cfc68b208423a00451640fad6744f974a9c9e8a62d89e9a5e47159/librt-0.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:86605d5bac340beb030cbc35859325982a79047ebdfba1e553719c7126a2389d", size = 86386, upload-time = "2025-11-29T14:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4d/9460e0b6d53cabeb78016e7e4e7d70dcd11fe90ef37c704856bd8a2ff533/librt-0.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98e4bbecbef8d2a60ecf731d735602feee5ac0b32117dbbc765e28b054bac912", size = 89064, upload-time = "2025-11-29T14:01:53.249Z" }, + { url = "https://files.pythonhosted.org/packages/20/f3/cb8410cbd34718fe7e11b006bacc8093d3f758c7d771c95613caebe7a9fc/librt-0.6.3-cp39-cp39-win32.whl", hash = "sha256:3caa0634c02d5ff0b2ae4a28052e0d8c5f20d497623dc13f629bd4a9e2a6efad", size = 19867, upload-time = "2025-11-29T14:01:54.201Z" }, + { url = "https://files.pythonhosted.org/packages/14/7e/521c046b3bc9316c408d159bc4f2c4be607280b3646416b953bdd4efda6f/librt-0.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:b47395091e7e0ece1e6ebac9b98bf0c9084d1e3d3b2739aa566be7e56e3f7bf2", size = 21408, upload-time = "2025-11-29T14:01:55.126Z" }, +] + [[package]] name = "mypy" -version = "1.15.0" +version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt" }, { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433, upload-time = "2025-02-05T03:49:29.145Z" }, - { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472, upload-time = "2025-02-05T03:49:16.986Z" }, - { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424, upload-time = "2025-02-05T03:49:46.908Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450, upload-time = "2025-02-05T03:50:05.89Z" }, - { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765, upload-time = "2025-02-05T03:49:33.56Z" }, - { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701, upload-time = "2025-02-05T03:49:38.981Z" }, - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload-time = "2025-02-05T03:50:28.25Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload-time = "2025-02-05T03:50:13.411Z" }, - { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload-time = "2025-02-05T03:50:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload-time = "2025-02-05T03:48:48.705Z" }, - { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload-time = "2025-02-05T03:49:03.628Z" }, - { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload-time = "2025-02-05T03:50:00.313Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" }, - { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" }, - { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" }, - { url = "https://files.pythonhosted.org/packages/5a/fa/79cf41a55b682794abe71372151dbbf856e3008f6767057229e6649d294a/mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078", size = 10737129, upload-time = "2025-02-05T03:50:24.509Z" }, - { url = "https://files.pythonhosted.org/packages/d3/33/dd8feb2597d648de29e3da0a8bf4e1afbda472964d2a4a0052203a6f3594/mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba", size = 9856335, upload-time = "2025-02-05T03:49:36.398Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b5/74508959c1b06b96674b364ffeb7ae5802646b32929b7701fc6b18447592/mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5", size = 11611935, upload-time = "2025-02-05T03:49:14.154Z" }, - { url = "https://files.pythonhosted.org/packages/6c/53/da61b9d9973efcd6507183fdad96606996191657fe79701b2c818714d573/mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b", size = 12365827, upload-time = "2025-02-05T03:48:59.458Z" }, - { url = "https://files.pythonhosted.org/packages/c1/72/965bd9ee89540c79a25778cc080c7e6ef40aa1eeac4d52cec7eae6eb5228/mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2", size = 12541924, upload-time = "2025-02-05T03:50:03.12Z" }, - { url = "https://files.pythonhosted.org/packages/46/d0/f41645c2eb263e6c77ada7d76f894c580c9ddb20d77f0c24d34273a4dab2/mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980", size = 9271176, upload-time = "2025-02-05T03:50:10.86Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, + { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, + { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, + { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, + { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, + { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, + { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/a7748ef43446163a93159d82bb270c6c4f3d94c1fcbdd2a29a7e439e74d7/mypy-1.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0dde5cb375cb94deff0d4b548b993bec52859d1651e073d63a1386d392a95495", size = 13094255, upload-time = "2025-11-28T15:47:14.282Z" }, + { url = "https://files.pythonhosted.org/packages/f5/0b/92ebf5abc83f559a35dcba3bd9227726b04b04178f1e521f38e647b930eb/mypy-1.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1cf9c59398db1c68a134b0b5354a09a1e124523f00bacd68e553b8bd16ff3299", size = 12161414, upload-time = "2025-11-28T15:45:03.302Z" }, + { url = "https://files.pythonhosted.org/packages/aa/03/19412f0a786722055a52c01b4c5d71e5b5443a89f6bbcdd445408240e217/mypy-1.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3210d87b30e6af9c8faed61be2642fcbe60ef77cec64fa1ef810a630a4cf671c", size = 12756782, upload-time = "2025-11-28T15:46:49.522Z" }, + { url = "https://files.pythonhosted.org/packages/cb/85/395d53c9098b251414b0448cdadcd3277523ff36f5abda6d26ff945dbdb3/mypy-1.19.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2c1101ab41d01303103ab6ef82cbbfedb81c1a060c868fa7cc013d573d37ab5", size = 13503492, upload-time = "2025-11-28T15:48:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/dd/33/1ab1113e3778617ae7aba66b4b537f90512bd279ff65b6c984fb91fbb2d3/mypy-1.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ea4fd21bb48f0da49e6d3b37ef6bd7e8228b9fe41bbf4d80d9364d11adbd43c", size = 13787703, upload-time = "2025-11-28T15:48:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/8b0821b3e0d538de1ad96c86502256c7326274d5cb74e0b373efaada273f/mypy-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:16f76ff3f3fd8137aadf593cb4607d82634fca675e8211ad75c43d86033ee6c6", size = 10049225, upload-time = "2025-11-28T15:45:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, ] [[package]] @@ -285,6 +376,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -468,7 +568,7 @@ dev = [ { name = "coverage" }, { name = "faker" }, { name = "isort", specifier = ">=6.0.1" }, - { name = "mypy" }, + { name = "mypy", specifier = ">=1.19.0" }, { name = "pytest" }, { name = "pytest-mock" }, { name = "python-dotenv" }, From aae9d7516f2bc525cfce15a6c7602173fbf53b27 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Thu, 4 Dec 2025 15:53:09 +0200 Subject: [PATCH 03/32] chore: add httpx --- pyproject.toml | 5 ++++- uv.lock | 55 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e363807..eba144b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,10 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -dependencies = ["requests", "typing-extensions"] +dependencies = [ + "httpx>=0.28.1", + "typing-extensions", +] dynamic = ["version"] [project.urls] diff --git a/uv.lock b/uv.lock index 994eaa3..d7d7d0e 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,20 @@ resolution-markers = [ "python_full_version < '3.10'", ] +[[package]] +name = "anyio" +version = "4.12.0" +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.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -196,6 +210,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/99/045b2dae19a01b9fbb23b9971bc04f4ef808e7f3a213d08c81067304a210/faker-37.3.0-py3-none-any.whl", hash = "sha256:48c94daa16a432f2d2bc803c7ff602509699fca228d13e97e379cd860a7e216e", size = 1942203, upload-time = "2025-05-14T15:24:16.159Z" }, ] +[[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 = "idna" version = "3.10" @@ -539,7 +590,7 @@ wheels = [ name = "typesense" source = { virtual = "." } dependencies = [ - { name = "requests" }, + { name = "httpx" }, { name = "typing-extensions" }, ] @@ -559,7 +610,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "requests" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "typing-extensions" }, ] From df27a3282aa499b0118a48cd7393690469bd31ed Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Thu, 4 Dec 2025 16:28:25 +0200 Subject: [PATCH 04/32] chore(tests): remove redudant mocked tests --- pyproject.toml | 1 - tests/alias_test.py | 49 ---------- tests/aliases_test.py | 56 ----------- tests/analytics_events_test.py | 20 ---- tests/analytics_rule_test.py | 25 ----- tests/analytics_rule_v1_test.py | 47 --------- tests/analytics_rules_test.py | 51 ---------- tests/analytics_rules_v1_test.py | 72 -------------- tests/collection_test.py | 150 ----------------------------- tests/collections_test.py | 125 ------------------------ tests/conversation_model_test.py | 50 ---------- tests/conversations_models_test.py | 59 ------------ tests/curation_set_test.py | 93 +----------------- tests/curation_sets_test.py | 68 ------------- tests/debug_test.py | 22 ----- tests/document_test.py | 51 ---------- tests/documents_test.py | 26 ----- tests/key_test.py | 41 -------- tests/keys_test.py | 69 ------------- tests/operations_test.py | 13 --- tests/override_test.py | 50 ---------- tests/overrides_test.py | 57 ----------- tests/stopwords_set_test.py | 47 --------- tests/stopwords_test.py | 54 ----------- tests/synonym_set_items_test.py | 43 --------- tests/synonym_set_test.py | 51 ---------- tests/synonym_sets_test.py | 68 ------------- tests/synonym_test.py | 49 ---------- tests/synonyms_test.py | 58 ----------- 29 files changed, 1 insertion(+), 1564 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eba144b..a5e0f82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ dev = [ "pytest", "coverage", "pytest-mock", - "requests-mock", "python-dotenv", "types-requests", "faker", diff --git a/tests/alias_test.py b/tests/alias_test.py index b3a74b6..a454561 100644 --- a/tests/alias_test.py +++ b/tests/alias_test.py @@ -1,9 +1,6 @@ """Tests for the Alias class.""" from __future__ import annotations - -import requests_mock - from tests.utils.object_assertions import ( assert_match_object, assert_object_lists_match, @@ -12,7 +9,6 @@ from typesense.alias import Alias from typesense.aliases import Aliases from typesense.api_call import ApiCall -from typesense.types.alias import AliasSchema def test_init(fake_api_call: ApiCall) -> None: @@ -32,51 +28,6 @@ def test_init(fake_api_call: ApiCall) -> None: assert alias._endpoint_path == "/aliases/company_alias" # noqa: WPS437 -def test_retrieve(fake_alias: Alias) -> None: - """Test that the Alias object can retrieve an alias.""" - json_response: AliasSchema = { - "collection_name": "companies", - "name": "company_alias", - } - - with requests_mock.Mocker() as mock: - mock.get( - "/aliases/company_alias", - json=json_response, - ) - - response = fake_alias.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url == "http://nearest:8108/aliases/company_alias" - ) - assert response == json_response - - -def test_delete(fake_alias: Alias) -> None: - """Test that the Alias object can delete an alias.""" - json_response: AliasSchema = { - "collection_name": "companies", - "name": "company_alias", - } - with requests_mock.Mocker() as mock: - mock.delete( - "/aliases/company_alias", - json=json_response, - ) - - response = fake_alias.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert ( - mock.request_history[0].url == "http://nearest:8108/aliases/company_alias" - ) - assert response == json_response - - def test_actual_retrieve( actual_aliases: Aliases, delete_all_aliases: None, diff --git a/tests/aliases_test.py b/tests/aliases_test.py index 314bf97..b1a1adf 100644 --- a/tests/aliases_test.py +++ b/tests/aliases_test.py @@ -1,9 +1,6 @@ """Tests for the Aliases class.""" from __future__ import annotations - -import requests_mock - from tests.utils.object_assertions import ( assert_match_object, assert_object_lists_match, @@ -11,7 +8,6 @@ ) from typesense.aliases import Aliases from typesense.api_call import ApiCall -from typesense.types.alias import AliasesResponseSchema, AliasSchema def test_init(fake_api_call: ApiCall) -> None: @@ -57,58 +53,6 @@ def test_get_existing_alias(fake_aliases: Aliases) -> None: assert alias is fetched_alias -def test_retrieve(fake_aliases: Aliases) -> None: - """Test that the Aliases object can retrieve aliases.""" - json_response: AliasesResponseSchema = { - "aliases": [ - { - "collection_name": "companies", - "name": "company_alias", - }, - ], - } - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/aliases", - json=json_response, - ) - - response = fake_aliases.retrieve() - - assert len(response) == 1 - assert response["aliases"][0] == { - "collection_name": "companies", - "name": "company_alias", - } - assert response == json_response - - -def test_create(fake_aliases: Aliases) -> None: - """Test that the Aliases object can create a alias.""" - json_response: AliasSchema = { - "collection_name": "companies", - "name": "company_alias", - } - - with requests_mock.Mocker() as mock: - mock.put( - "http://nearest:8108/aliases/company_alias", - json=json_response, - ) - - fake_aliases.upsert( - "company_alias", - {"collection_name": "companies", "name": "company_alias"}, - ) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "PUT" - assert mock.last_request.url == "http://nearest:8108/aliases/company_alias" - assert mock.last_request.json() == json_response - - def test_actual_create(actual_aliases: Aliases, delete_all_aliases: None) -> None: """Test that the Aliases object can create an alias on Typesense Server.""" response = actual_aliases.upsert("company_alias", {"collection_name": "companies"}) diff --git a/tests/analytics_events_test.py b/tests/analytics_events_test.py index b970e2c..965df58 100644 --- a/tests/analytics_events_test.py +++ b/tests/analytics_events_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.version import is_v30_or_above from typesense.client import Client @@ -50,18 +49,6 @@ def test_actual_create_event( actual_client.analytics.rules["company_analytics_rule"].delete() -def test_create_event(fake_client: Client) -> None: - event: AnalyticsEvent = { - "name": "company_analytics_rule", - "event_type": "query", - "data": {"user_id": "user-1", "q": "apple"}, - } - with requests_mock.Mocker() as mock: - mock.post("http://nearest:8108/analytics/events", json={"ok": True}) - resp = fake_client.analytics.events.create(event) - assert resp["ok"] is True - - def test_status(actual_client: Client, delete_all: None) -> None: status = actual_client.analytics.events.status() assert isinstance(status, dict) @@ -140,10 +127,3 @@ def test_acutal_retrieve_events( def test_acutal_flush(actual_client: Client, delete_all: None) -> None: resp = actual_client.analytics.events.flush() assert resp["ok"] in [True, False] - - -def test_flush(fake_client: Client) -> None: - with requests_mock.Mocker() as mock: - mock.post("http://nearest:8108/analytics/flush", json={"ok": True}) - resp = fake_client.analytics.events.flush() - assert resp["ok"] is True diff --git a/tests/analytics_rule_test.py b/tests/analytics_rule_test.py index 199e7ae..525aa27 100644 --- a/tests/analytics_rule_test.py +++ b/tests/analytics_rule_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.version import is_v30_or_above from typesense.client import Client @@ -24,30 +23,6 @@ ) -def test_rule_retrieve(fake_api_call) -> None: - rule = AnalyticsRule(fake_api_call, "company_analytics_rule") - expected = {"name": "company_analytics_rule"} - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/analytics/rules/company_analytics_rule", - json=expected, - ) - resp = rule.retrieve() - assert resp == expected - - -def test_rule_delete(fake_api_call) -> None: - rule = AnalyticsRule(fake_api_call, "company_analytics_rule") - expected = {"name": "company_analytics_rule"} - with requests_mock.Mocker() as mock: - mock.delete( - "http://nearest:8108/analytics/rules/company_analytics_rule", - json=expected, - ) - resp = rule.delete() - assert resp == expected - - def test_actual_rule_retrieve( actual_analytics_rules: AnalyticsRules, delete_all: None, diff --git a/tests/analytics_rule_v1_test.py b/tests/analytics_rule_v1_test.py index d30b002..071d3e8 100644 --- a/tests/analytics_rule_v1_test.py +++ b/tests/analytics_rule_v1_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from tests.utils.version import is_v30_or_above @@ -46,66 +45,24 @@ def test_init(fake_api_call: ApiCall) -> None: ) -def test_retrieve(fake_analytics_rule: AnalyticsRuleV1) -> None: - """Test that the AnalyticsRuleV1 object can retrieve an analytics_rule.""" - json_response: RuleSchemaForQueries = { "name": "company_analytics_rule", "params": { - "destination": { - "collection": "companies_queries", - }, "source": {"collections": ["companies"]}, }, "type": "nohits_queries", } - with requests_mock.Mocker() as mock: - mock.get( - "/analytics/rules/company_analytics_rule", - json=json_response, - ) - - response = fake_analytics_rule.retrieve() - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url - == "http://nearest:8108/analytics/rules/company_analytics_rule" - ) - assert response == json_response -def test_delete(fake_analytics_rule: AnalyticsRuleV1) -> None: - """Test that the AnalyticsRuleV1 object can delete an analytics_rule.""" - json_response: RuleDeleteSchema = { "name": "company_analytics_rule", } - with requests_mock.Mocker() as mock: - mock.delete( - "/analytics/rules/company_analytics_rule", - json=json_response, - ) - - response = fake_analytics_rule.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert ( - mock.request_history[0].url - == "http://nearest:8108/analytics/rules/company_analytics_rule" - ) - assert response == json_response -def test_actual_retrieve( - actual_analytics_rules: AnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_analytics_rule_v1: None, ) -> None: - """Test that the AnalyticsRuleV1 object can retrieve a rule from Typesense Server.""" - response = actual_analytics_rules["company_analytics_rule"].retrieve() expected: RuleSchemaForQueries = { "name": "company_analytics_rule", @@ -120,14 +77,10 @@ def test_actual_retrieve( assert response == expected -def test_actual_delete( - actual_analytics_rules: AnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_analytics_rule_v1: None, ) -> None: - """Test that the AnalyticsRuleV1 object can delete a rule from Typesense Server.""" - response = actual_analytics_rules["company_analytics_rule"].delete() expected: RuleDeleteSchema = { "name": "company_analytics_rule", diff --git a/tests/analytics_rules_test.py b/tests/analytics_rules_test.py index 70f16f5..fb3b35a 100644 --- a/tests/analytics_rules_test.py +++ b/tests/analytics_rules_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.version import is_v30_or_above from typesense.client import Client @@ -37,56 +36,6 @@ def test_rule_getitem(fake_api_call) -> None: assert rule._endpoint_path == "/analytics/rules/company_analytics_rule" -def test_rules_create(fake_api_call) -> None: - rules = AnalyticsRules(fake_api_call) - body: AnalyticsRuleCreate = { - "name": "company_analytics_rule", - "type": "popular_queries", - "collection": "companies", - "event_type": "search", - "params": {"destination_collection": "companies_queries", "limit": 1000}, - } - with requests_mock.Mocker() as mock: - mock.post("http://nearest:8108/analytics/rules", json=body) - resp = rules.create(body) - assert resp == body - - -def test_rules_retrieve_with_tag(fake_api_call) -> None: - rules = AnalyticsRules(fake_api_call) - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/analytics/rules?rule_tag=homepage", - json=[{"name": "rule1", "rule_tag": "homepage"}], - ) - resp = rules.retrieve(rule_tag="homepage") - assert isinstance(resp, list) - assert resp[0]["rule_tag"] == "homepage" - - -def test_rules_upsert(fake_api_call) -> None: - rules = AnalyticsRules(fake_api_call) - with requests_mock.Mocker() as mock: - mock.put( - "http://nearest:8108/analytics/rules/company_analytics_rule", - json={"name": "company_analytics_rule"}, - ) - resp = rules.upsert("company_analytics_rule", {"params": {}}) - assert resp["name"] == "company_analytics_rule" - - -def test_rules_retrieve(fake_api_call) -> None: - rules = AnalyticsRules(fake_api_call) - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/analytics/rules", - json=[{"name": "company_analytics_rule"}], - ) - resp = rules.retrieve() - assert isinstance(resp, list) - assert resp[0]["name"] == "company_analytics_rule" - - def test_actual_create( actual_analytics_rules: AnalyticsRules, delete_all: None, diff --git a/tests/analytics_rules_v1_test.py b/tests/analytics_rules_v1_test.py index 7eb2749..76263f9 100644 --- a/tests/analytics_rules_v1_test.py +++ b/tests/analytics_rules_v1_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from tests.utils.version import is_v30_or_above @@ -76,93 +75,30 @@ def test_get_existing_analytics_rule(fake_analytics_rules: AnalyticsRulesV1) -> assert analytics_rule is fetched_analytics_rule -def test_retrieve(fake_analytics_rules: AnalyticsRulesV1) -> None: - """Test that the AnalyticsRulesV1 object can retrieve analytics_rules.""" - json_response: RulesRetrieveSchema = { - "rules": [ - { - "name": "company_analytics_rule", - "params": { - "destination": { - "collection": "companies_queries", - }, - "source": {"collections": ["companies"]}, }, - "type": "nohits_queries", }, - ], - } - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/analytics/rules", - json=json_response, - ) - - response = fake_analytics_rules.retrieve() - - assert len(response) == 1 - assert response["rules"][0] == json_response.get("rules")[0] - assert response == json_response - -def test_create(fake_analytics_rules: AnalyticsRulesV1) -> None: - """Test that the AnalyticsRulesV1 object can create a analytics_rule.""" - json_response: RuleCreateSchemaForQueries = { "name": "company_analytics_rule", "params": { - "destination": { - "collection": "companies_queries", - }, "source": {"collections": ["companies"]}, }, - "type": "nohits_queries", } - with requests_mock.Mocker() as mock: - mock.post( - "http://nearest:8108/analytics/rules", - json=json_response, - ) - fake_analytics_rules.create( - rule={ - "params": { - "destination": { - "collection": "companies_queries", - }, - "source": {"collections": ["companies"]}, }, - "type": "nohits_queries", - "name": "company_analytics_rule", }, - ) - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "POST" - assert mock.last_request.url == "http://nearest:8108/analytics/rules" - assert mock.last_request.json() == { "params": { - "destination": { - "collection": "companies_queries", - }, "source": {"collections": ["companies"]}, }, "type": "nohits_queries", - "name": "company_analytics_rule", - } -def test_actual_create( - actual_analytics_rules: AnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_collection: None, create_query_collection: None, ) -> None: - """Test that the AnalyticsRulesV1 object can create an analytics_rule on Typesense Server.""" - response = actual_analytics_rules.create( rule={ "name": "company_analytics_rule", "type": "nohits_queries", @@ -185,14 +121,10 @@ def test_actual_create( } -def test_actual_update( - actual_analytics_rules: AnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_analytics_rule_v1: None, ) -> None: - """Test that the AnalyticsRulesV1 object can update an analytics_rule on Typesense Server.""" - response = actual_analytics_rules.upsert( "company_analytics_rule", { "type": "popular_queries", @@ -215,14 +147,10 @@ def test_actual_update( } -def test_actual_retrieve( - actual_analytics_rules: AnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_analytics_rule_v1: None, ) -> None: - """Test that the AnalyticsRulesV1 object can retrieve the rules from Typesense Server.""" - response = actual_analytics_rules.retrieve() assert len(response["rules"]) == 1 assert_match_object( response["rules"][0], diff --git a/tests/collection_test.py b/tests/collection_test.py index 56c4429..d2cccfd 100644 --- a/tests/collection_test.py +++ b/tests/collection_test.py @@ -2,10 +2,6 @@ from __future__ import annotations -import time - -import requests_mock - from tests.utils.object_assertions import ( assert_match_object, assert_object_lists_match, @@ -35,152 +31,6 @@ def test_init(fake_api_call: ApiCall) -> None: assert collection._endpoint_path == "/collections/companies" # noqa: WPS437 -def test_retrieve(fake_collection: Collection) -> None: - """Test that the Collection object can retrieve a collection.""" - time_now = int(time.time()) - - json_response: CollectionSchema = { - "created_at": time_now, - "default_sorting_field": "num_employees", - "enable_nested_fields": False, - "fields": [ - { - "name": "company_name", - "type": "string", - }, - { - "name": "num_employees", - "type": "int32", - }, - ], - "name": "companies", - "num_documents": 0, - "symbols_to_index": [], - "token_separators": [], - "synonym_sets": [], - "curation_sets": [], - } - - with requests_mock.mock() as mock: - mock.get( - "http://nearest:8108/collections/companies", - json=json_response, - ) - - response = fake_collection.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url == "http://nearest:8108/collections/companies" - ) - - assert response == json_response - - -def test_update(fake_collection: Collection) -> None: - """Test that the Collection object can update a collection.""" - json_response: CollectionSchema = { - "created_at": 1619711487, - "default_sorting_field": "num_employees", - "enable_nested_fields": False, - "fields": [ - { - "name": "company_name", - "type": "string", - }, - { - "name": "num_employees", - "type": "int32", - }, - { - "name": "num_locations", - "type": "int32", - }, - ], - "name": "companies", - "num_documents": 0, - "symbols_to_index": [], - "token_separators": [], - "synonym_sets": [], - "curation_sets": [], - } - - with requests_mock.mock() as mock: - mock.patch( - "http://nearest:8108/collections/companies", - json=json_response, - ) - - response = fake_collection.update( - schema_change={ - "fields": [ - { - "name": "num_locations", - "type": "int32", - }, - ], - }, - ) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "PATCH" - assert mock.last_request.url == "http://nearest:8108/collections/companies" - assert mock.last_request.json() == { - "fields": [ - { - "name": "num_locations", - "type": "int32", - }, - ], - } - assert response == json_response - - -def test_delete(fake_collection: Collection) -> None: - """Test that the Collection object can delete a collection.""" - json_response: CollectionSchema = { - "created_at": 1619711487, - "default_sorting_field": "num_employees", - "enable_nested_fields": False, - "fields": [ - { - "name": "company_name", - "type": "string", - }, - { - "name": "num_employees", - "type": "int32", - }, - { - "name": "num_locations", - "type": "int32", - }, - ], - "name": "companies", - "num_documents": 0, - "symbols_to_index": [], - "token_separators": [], - "synonym_sets": [], - "curation_sets": [], - } - - with requests_mock.mock() as mock: - mock.delete( - "http://nearest:8108/collections/companies", - json=json_response, - ) - - response = fake_collection.delete() - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "DELETE" - assert mock.last_request.url == "http://nearest:8108/collections/companies" - assert response == json_response - - def test_actual_retrieve( actual_collections: Collections, delete_all: None, diff --git a/tests/collections_test.py b/tests/collections_test.py index d742652..8e3d7ef 100644 --- a/tests/collections_test.py +++ b/tests/collections_test.py @@ -61,131 +61,6 @@ def test_get_existing_collection(fake_collections: Collections) -> None: assert collection is fetched_collection -def test_retrieve(fake_collections: Collections) -> None: - """Test that the Collections object can retrieve collections.""" - json_response: typing.List[CollectionSchema] = [ - { - "created_at": 1619711487, - "default_sorting_field": "num_employees", - "enable_nested_fields": False, - "fields": [ - { - "name": "company_name", - "type": "string", - }, - { - "name": "num_employees", - "type": "int32", - }, - { - "name": "num_locations", - "type": "int32", - }, - ], - "name": "companies", - "num_documents": 0, - "symbols_to_index": [], - "token_separators": [], - "synonym_sets": [], - }, - { - "created_at": 1619711488, - "default_sorting_field": "likes", - "enable_nested_fields": False, - "fields": [ - { - "name": "name", - "type": "string", - }, - { - "name": "likes", - "type": "int32", - }, - ], - "name": "posts", - "num_documents": 0, - "symbols_to_index": [], - "token_separators": [], - "synonym_sets": [], - }, - ] - with requests_mock.Mocker() as mock: - mock.get("http://nearest:8108/collections", json=json_response) - - response = fake_collections.retrieve() - - assert len(response) == 2 - assert response[0]["name"] == "companies" - assert response[1]["name"] == "posts" - assert response == json_response - - -def test_create(fake_collections: Collections) -> None: - """Test that the Collections object can create a collection.""" - json_response: CollectionSchema = { - "created_at": 1619711487, - "default_sorting_field": "num_employees", - "enable_nested_fields": False, - "fields": [ - { - "name": "company_name", - "type": "string", - }, - { - "name": "num_employees", - "type": "int32", - }, - ], - "name": "companies", - "num_documents": 0, - "symbols_to_index": [], - "token_separators": [], - "synonym_sets": [], - } - - with requests_mock.Mocker() as mock: - mock.post( - "http://nearest:8108/collections", - json=json_response, - ) - - fake_collections.create( - { - "name": "companies", - "fields": [ - { - "name": "company_name", - "type": "string", - }, - { - "name": "num_employees", - "type": "int32", - }, - ], - "default_sorting_field": "num_employees", - }, - ) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "POST" - assert mock.last_request.url == "http://nearest:8108/collections" - assert mock.last_request.json() == { - "name": "companies", - "fields": [ - { - "name": "company_name", - "type": "string", - }, - { - "name": "num_employees", - "type": "int32", - }, - ], - "default_sorting_field": "num_employees", - } - - def test_actual_create(actual_collections: Collections, delete_all: None) -> None: """Test that the Collections object can create a collection on Typesense Server.""" expected: CollectionSchema = { diff --git a/tests/conversation_model_test.py b/tests/conversation_model_test.py index 43b8bab..ead4817 100644 --- a/tests/conversation_model_test.py +++ b/tests/conversation_model_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from dotenv import load_dotenv from tests.utils.object_assertions import ( @@ -45,55 +44,6 @@ def test_init(fake_api_call: ApiCall) -> None: ) -def test_retrieve(fake_conversation_model: ConversationModel) -> None: - """Test that the ConversationModel object can retrieve a conversation_model.""" - json_response: ConversationModelSchema = { - "api_key": "abc", - "id": "conversation_model_id", - "max_bytes": 1000000, - "model_name": "conversation_model_name", - "system_prompt": "This is a system prompt", - } - - with requests_mock.Mocker() as mock: - mock.get( - "/conversations/models/conversation_model_id", - json=json_response, - ) - - response = fake_conversation_model.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url - == "http://nearest:8108/conversations/models/conversation_model_id" - ) - assert response == json_response - - -def test_delete(fake_conversation_model: ConversationModel) -> None: - """Test that the ConversationModel object can delete a conversation_model.""" - json_response: ConversationModelDeleteSchema = { - "id": "conversation_model_id", - } - with requests_mock.Mocker() as mock: - mock.delete( - "/conversations/models/conversation_model_id", - json=json_response, - ) - - response = fake_conversation_model.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert ( - mock.request_history[0].url - == "http://nearest:8108/conversations/models/conversation_model_id" - ) - assert response == json_response - - @pytest.mark.open_ai def test_actual_retrieve( actual_conversations_models: ConversationsModels, diff --git a/tests/conversations_models_test.py b/tests/conversations_models_test.py index 32eb2b9..cf76e04 100644 --- a/tests/conversations_models_test.py +++ b/tests/conversations_models_test.py @@ -6,7 +6,6 @@ import sys import pytest -import requests_mock if sys.version_info >= (3, 11): import typing @@ -77,64 +76,6 @@ def test_get_existing_conversations_model( assert conversations_model is fetched_conversations_model -def test_retrieve(fake_conversations_models: ConversationsModels) -> None: - """Test that the ConversationsModels object can retrieve conversations_models.""" - json_response: typing.List[ConversationModelSchema] = [ - { - "api_key": "abc", - "id": "1", - "max_bytes": 1000000, - "model_name": "openAI-gpt-3", - "system_prompt": "This is a system prompt", - }, - ] - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/conversations/models", - json=json_response, - ) - - response = fake_conversations_models.retrieve() - - assert len(response) == 1 - assert response[0] == json_response[0] - assert response == json_response - - -def test_create(fake_conversations_models: ConversationsModels) -> None: - """Test that the ConversationsModels object can create a conversations_model.""" - json_response: ConversationModelSchema = { - "api_key": "abc", - "id": "1", - "max_bytes": 1000000, - "model_name": "openAI-gpt-3", - "system_prompt": "This is a system prompt", - } - - with requests_mock.Mocker() as mock: - mock.post( - "http://nearest:8108/conversations/models", - json=json_response, - ) - - fake_conversations_models.create( - model={ - "api_key": "abc", - "id": "1", - "max_bytes": 1000000, - "model_name": "openAI-gpt-3", - "system_prompt": "This is a system prompt", - }, - ) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "POST" - assert mock.last_request.url == "http://nearest:8108/conversations/models" - assert mock.last_request.json() == json_response - - @pytest.mark.open_ai def test_actual_create( actual_conversations_models: ConversationsModels, diff --git a/tests/curation_set_test.py b/tests/curation_set_test.py index d8c4075..f441465 100644 --- a/tests/curation_set_test.py +++ b/tests/curation_set_test.py @@ -3,19 +3,12 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.version import is_v30_or_above from typesense.client import Client from typesense.curation_set import CurationSet from typesense.curation_sets import CurationSets -from typesense.types.curation_set import ( - CurationItemDeleteSchema, - CurationItemSchema, - CurationSetDeleteSchema, - CurationSetListItemResponseSchema, - CurationSetSchema, -) +from typesense.types.curation_set import CurationItemSchema pytestmark = pytest.mark.skipif( not is_v30_or_above( @@ -35,90 +28,6 @@ def test_paths(fake_curation_set: CurationSet) -> None: assert fake_curation_set._items_path == "/curation_sets/products/items" # noqa: WPS437 -def test_retrieve(fake_curation_set: CurationSet) -> None: - json_response: CurationSetSchema = { - "name": "products", - "items": [], - } - with requests_mock.Mocker() as mock: - mock.get( - "/curation_sets/products", - json=json_response, - ) - res = fake_curation_set.retrieve() - assert res == json_response - - -def test_delete(fake_curation_set: CurationSet) -> None: - json_response: CurationSetDeleteSchema = {"name": "products"} - with requests_mock.Mocker() as mock: - mock.delete( - "/curation_sets/products", - json=json_response, - ) - res = fake_curation_set.delete() - assert res == json_response - - -def test_list_items(fake_curation_set: CurationSet) -> None: - json_response: CurationSetListItemResponseSchema = [ - { - "id": "rule-1", - "rule": {"query": "shoe", "match": "contains"}, - "includes": [{"id": "123", "position": 1}], - } - ] - with requests_mock.Mocker() as mock: - mock.get( - "/curation_sets/products/items?limit=10&offset=0", - json=json_response, - ) - res = fake_curation_set.list_items(limit=10, offset=0) - assert res == json_response - - -def test_get_item(fake_curation_set: CurationSet) -> None: - json_response: CurationItemSchema = { - "id": "rule-1", - "rule": {"query": "shoe", "match": "contains"}, - "includes": [{"id": "123", "position": 1}], - } - with requests_mock.Mocker() as mock: - mock.get( - "/curation_sets/products/items/rule-1", - json=json_response, - ) - res = fake_curation_set.get_item("rule-1") - assert res == json_response - - -def test_upsert_item(fake_curation_set: CurationSet) -> None: - payload: CurationItemSchema = { - "id": "rule-1", - "rule": {"query": "shoe", "match": "contains"}, - "includes": [{"id": "123", "position": 1}], - } - json_response = payload - with requests_mock.Mocker() as mock: - mock.put( - "/curation_sets/products/items/rule-1", - json=json_response, - ) - res = fake_curation_set.upsert_item("rule-1", payload) - assert res == json_response - - -def test_delete_item(fake_curation_set: CurationSet) -> None: - json_response: CurationItemDeleteSchema = {"id": "rule-1"} - with requests_mock.Mocker() as mock: - mock.delete( - "/curation_sets/products/items/rule-1", - json=json_response, - ) - res = fake_curation_set.delete_item("rule-1") - assert res == json_response - - def test_actual_retrieve( actual_curation_sets: CurationSets, delete_all_curation_sets: None, diff --git a/tests/curation_sets_test.py b/tests/curation_sets_test.py index 88c70bf..3168bf3 100644 --- a/tests/curation_sets_test.py +++ b/tests/curation_sets_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import ( assert_match_object, @@ -14,7 +13,6 @@ from typesense.api_call import ApiCall from typesense.client import Client from typesense.curation_sets import CurationSets -from typesense.types.curation_set import CurationSetSchema, CurationSetUpsertSchema pytestmark = pytest.mark.skipif( not is_v30_or_above( @@ -40,72 +38,6 @@ def test_init(fake_api_call: ApiCall) -> None: ) -def test_retrieve(fake_curation_sets: CurationSets) -> None: - """Test that the CurationSets object can retrieve curation sets.""" - json_response = [ - { - "name": "products", - "items": [ - { - "id": "rule-1", - "rule": {"query": "shoe", "match": "contains"}, - "includes": [{"id": "123", "position": 1}], - } - ], - } - ] - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/curation_sets", - json=json_response, - ) - - response = fake_curation_sets.retrieve() - - assert isinstance(response, list) - assert len(response) == 1 - assert response == json_response - - -def test_upsert(fake_curation_sets: CurationSets) -> None: - """Test that the CurationSets object can upsert a curation set.""" - json_response: CurationSetSchema = { - "name": "products", - "items": [ - { - "id": "rule-1", - "rule": {"query": "shoe", "match": "contains"}, - "includes": [{"id": "123", "position": 1}], - } - ], - } - - with requests_mock.Mocker() as mock: - mock.put( - "http://nearest:8108/curation_sets/products", - json=json_response, - ) - - payload: CurationSetUpsertSchema = { - "items": [ - { - "id": "rule-1", - "rule": {"query": "shoe", "match": "contains"}, - "includes": [{"id": "123", "position": 1}], - } - ] - } - response = fake_curation_sets["products"].upsert(payload) - - assert response == json_response - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "PUT" - assert mock.last_request.url == "http://nearest:8108/curation_sets/products" - assert mock.last_request.json() == payload - - def test_actual_upsert( actual_curation_sets: CurationSets, delete_all_curation_sets: None, diff --git a/tests/debug_test.py b/tests/debug_test.py index 37c593a..5415f84 100644 --- a/tests/debug_test.py +++ b/tests/debug_test.py @@ -1,13 +1,9 @@ """Tests for the Debug class.""" from __future__ import annotations - -import requests_mock - from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.api_call import ApiCall from typesense.debug import Debug -from typesense.types.debug import DebugResponseSchema def test_init(fake_api_call: ApiCall) -> None: @@ -28,24 +24,6 @@ def test_init(fake_api_call: ApiCall) -> None: assert debug.resource_path == "/debug" # noqa: WPS437 -def test_retrieve(fake_debug: Debug) -> None: - """Test that the Debug object can retrieve a debug.""" - json_response: DebugResponseSchema = {"state": 1, "version": "27.1"} - - with requests_mock.Mocker() as mock: - mock.get( - "/debug", - json=json_response, - ) - - response = fake_debug.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert mock.request_history[0].url == "http://nearest:8108/debug" - assert response == json_response - - def test_actual_retrieve(actual_debug: Debug) -> None: """Test that the Debug object can retrieve a debug on Typesense server and verify response structure.""" response = actual_debug.retrieve() diff --git a/tests/document_test.py b/tests/document_test.py index ac3042c..fcd40c3 100644 --- a/tests/document_test.py +++ b/tests/document_test.py @@ -3,9 +3,7 @@ from __future__ import annotations import pytest -import requests_mock -from tests.fixtures.document_fixtures import Companies from tests.utils.object_assertions import ( assert_match_object, assert_object_lists_match, @@ -37,55 +35,6 @@ def test_init(fake_api_call: ApiCall) -> None: ) -def test_retrieve(fake_document: Document) -> None: - """Test that the Document object can retrieve an document.""" - json_response: Companies = { - "company_name": "Company", - "id": "0", - "num_employees": 10, - } - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/collections/companies/documents/0", - json=json_response, - ) - - response = fake_document.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url - == "http://nearest:8108/collections/companies/documents/0" - ) - assert response == json_response - - -def test_delete(fake_document: Document) -> None: - """Test that the Document object can delete an document.""" - json_response: Companies = { - "company_name": "Company", - "id": "0", - "num_employees": 10, - } - with requests_mock.Mocker() as mock: - mock.delete( - "http://nearest:8108/collections/companies/documents/0", - json=json_response, - ) - - response = fake_document.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert ( - mock.request_history[0].url - == "http://nearest:8108/collections/companies/documents/0" - ) - assert response == json_response - - def test_actual_update( actual_documents: Documents, delete_all: None, diff --git a/tests/documents_test.py b/tests/documents_test.py index 994845a..e355869 100644 --- a/tests/documents_test.py +++ b/tests/documents_test.py @@ -67,32 +67,6 @@ def test_get_existing_document(fake_documents: Documents) -> None: assert document is fetched_document -def test_create( - actual_documents: Documents[Companies], - actual_api_call: ApiCall, - delete_all: None, - create_collection: None, - mocker: MockFixture, -) -> None: - """Test that the Documents object can create a document on Typesense server.""" - company: Companies = { - "company_name": "Typesense", - "id": "1", - "num_employees": 25, - } - spy = mocker.spy(actual_api_call, "post") - response = actual_documents.create(company) - expected = company - assert response == expected - spy.assert_called_once_with( - "/collections/companies/documents/", - body=company, - params={"action": "create"}, - as_json=True, - entity_type=typing.Dict[str, str], - ) - - def test_upsert( actual_documents: Documents[Companies], actual_api_call: ApiCall, diff --git a/tests/key_test.py b/tests/key_test.py index 5c06e16..d001dd9 100644 --- a/tests/key_test.py +++ b/tests/key_test.py @@ -2,7 +2,6 @@ from __future__ import annotations -import requests_mock from tests.utils.object_assertions import ( assert_match_object, @@ -12,7 +11,6 @@ from typesense.api_call import ApiCall from typesense.key import Key from typesense.keys import Keys -from typesense.types.key import ApiKeyDeleteSchema, ApiKeySchema def test_init(fake_api_call: ApiCall) -> None: @@ -32,45 +30,6 @@ def test_init(fake_api_call: ApiCall) -> None: assert key._endpoint_path == "/keys/3" # noqa: WPS437 -def test_retrieve(fake_key: Key) -> None: - """Test that the Key object can retrieve an key.""" - json_response: ApiKeySchema = { - "actions": ["documents:search"], - "collections": ["companies"], - "description": "Search-only key", - } - - with requests_mock.Mocker() as mock: - mock.get( - "/keys/1", - json=json_response, - ) - - response = fake_key.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert mock.request_history[0].url == "http://nearest:8108/keys/1" - assert response == json_response - - -def test_delete(fake_key: Key) -> None: - """Test that the Key object can delete an key.""" - json_response: ApiKeyDeleteSchema = {"id": 1} - with requests_mock.Mocker() as mock: - mock.delete( - "/keys/1", - json=json_response, - ) - - response = fake_key.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert mock.request_history[0].url == "http://nearest:8108/keys/1" - assert response == json_response - - def test_actual_retrieve( actual_keys: Keys, delete_all_keys: None, diff --git a/tests/keys_test.py b/tests/keys_test.py index 019d17d..2da40b5 100644 --- a/tests/keys_test.py +++ b/tests/keys_test.py @@ -6,9 +6,7 @@ import hashlib import hmac import json -import time -import requests_mock from tests.utils.object_assertions import ( assert_match_object, @@ -17,7 +15,6 @@ ) from typesense.api_call import ApiCall from typesense.keys import Keys -from typesense.types.key import ApiKeyRetrieveSchema def test_init(fake_api_call: ApiCall) -> None: @@ -62,72 +59,6 @@ def test_get_existing_key(fake_keys: Keys) -> None: assert key is fetched_key -def test_retrieve(fake_keys: Keys) -> None: - """Test that the Keys object can retrieve keys.""" - json_response: ApiKeyRetrieveSchema = { - "keys": [ - { - "actions": ["documents:search"], - "collections": ["companies"], - "description": "Search-only key", - "expires_at": int(time.time()) + 3600, - "id": 1, - "value_prefix": "asdf", - }, - ], - } - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/keys", - json=json_response, - ) - - response = fake_keys.retrieve() - - assert len(response) == 1 - assert response["keys"][0] == json_response.get("keys")[0] - assert response == json_response - - -def test_create(fake_keys: Keys) -> None: - """Test that the Keys object can create a key.""" - json_response: ApiKeyRetrieveSchema = { - "keys": [ - { - "actions": ["documents:search"], - "collections": ["companies"], - "description": "Search-only key", - "expires_at": int(time.time()) + 3600, - "id": 1, - "value_prefix": "asdf", - }, - ], - } - - with requests_mock.Mocker() as mock: - mock.post( - "http://nearest:8108/keys", - json=json_response, - ) - - fake_keys.create( - schema={ - "actions": ["documents:search"], - "collections": ["companies"], - }, - ) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "POST" - assert mock.last_request.url == "http://nearest:8108/keys" - assert mock.last_request.json() == { - "actions": ["documents:search"], - "collections": ["companies"], - } - - def test_actual_create( actual_keys: Keys, ) -> None: diff --git a/tests/operations_test.py b/tests/operations_test.py index 34bb74c..c6cfaf7 100644 --- a/tests/operations_test.py +++ b/tests/operations_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.api_call import ApiCall @@ -68,18 +67,6 @@ def test_health(actual_operations: Operations) -> None: assert response -def test_health_not_dict(fake_operations: Operations) -> None: - """Test that the Operations object can perform the health operation.""" - with requests_mock.Mocker() as mock: - mock.get( - "/health", - json="ok", - ) - - response = fake_operations.is_healthy() - assert not response - - def test_log_slow_requests_time_ms(actual_operations: Operations) -> None: """Test that the Operations object can perform the log_slow_requests_time_ms operation.""" response = actual_operations.toggle_slow_request_log( diff --git a/tests/override_test.py b/tests/override_test.py index eba0dee..b47619c 100644 --- a/tests/override_test.py +++ b/tests/override_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import ( assert_match_object, @@ -52,55 +51,6 @@ def test_init(fake_api_call: ApiCall) -> None: ) -def test_retrieve(fake_override: Override) -> None: - """Test that the Override object can retrieve an override.""" - json_response: OverrideSchema = { - "rule": { - "match": "contains", - "query": "companies", - }, - "filter_by": "num_employees>10", - } - - with requests_mock.Mocker() as mock: - mock.get( - "/collections/companies/overrides/company_override", - json=json_response, - ) - - response = fake_override.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url - == "http://nearest:8108/collections/companies/overrides/company_override" - ) - assert response == json_response - - -def test_delete(fake_override: Override) -> None: - """Test that the Override object can delete an override.""" - json_response: OverrideDeleteSchema = { - "id": "company_override", - } - with requests_mock.Mocker() as mock: - mock.delete( - "/collections/companies/overrides/company_override", - json=json_response, - ) - - response = fake_override.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert ( - mock.request_history[0].url - == "http://nearest:8108/collections/companies/overrides/company_override" - ) - assert response == {"id": "company_override"} - - def test_actual_retrieve( actual_collections: Collections, delete_all: None, diff --git a/tests/overrides_test.py b/tests/overrides_test.py index e543bea..cd6fe32 100644 --- a/tests/overrides_test.py +++ b/tests/overrides_test.py @@ -2,7 +2,6 @@ from __future__ import annotations -import requests_mock import pytest from tests.utils.object_assertions import ( @@ -76,62 +75,6 @@ def test_get_existing_override(fake_overrides: Overrides) -> None: assert override is fetched_override -def test_retrieve(fake_overrides: Overrides) -> None: - """Test that the Overrides object can retrieve overrides.""" - json_response: OverrideRetrieveSchema = { - "overrides": [ - { - "id": "company_override", - "rule": {"match": "exact", "query": "companies"}, - }, - ], - } - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/collections/companies/overrides/", - json=json_response, - ) - - response = fake_overrides.retrieve() - - assert len(response) == 1 - assert response["overrides"][0] == { - "id": "company_override", - "rule": {"match": "exact", "query": "companies"}, - } - assert response == json_response - - -def test_create(fake_overrides: Overrides) -> None: - """Test that the Overrides object can create a override.""" - json_response: OverrideSchema = { - "id": "company_override", - "rule": {"match": "exact", "query": "companies"}, - } - - with requests_mock.Mocker() as mock: - mock.put( - "http://nearest:8108/collections/companies/overrides/company_override", - json=json_response, - ) - - fake_overrides.upsert( - "company_override", - {"rule": {"match": "exact", "query": "companies"}}, - ) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "PUT" - assert ( - mock.last_request.url - == "http://nearest:8108/collections/companies/overrides/company_override" - ) - assert mock.last_request.json() == { - "rule": {"match": "exact", "query": "companies"}, - } - - def test_actual_create( actual_overrides: Overrides, delete_all: None, diff --git a/tests/stopwords_set_test.py b/tests/stopwords_set_test.py index 0ddcf9a..b2d1af2 100644 --- a/tests/stopwords_set_test.py +++ b/tests/stopwords_set_test.py @@ -2,7 +2,6 @@ from __future__ import annotations -import requests_mock from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.api_call import ApiCall @@ -28,52 +27,6 @@ def test_init(fake_api_call: ApiCall) -> None: assert stopword_set._endpoint_path == "/stopwords/company_stopwords" # noqa: WPS437 -def test_retrieve(fake_stopwords_set: StopwordsSet) -> None: - """Test that the StopwordsSet object can retrieve an stopword_set.""" - json_response: StopwordSchema = { - "id": "company_stopwords", - "stopwords": ["a", "an", "the"], - } - - with requests_mock.Mocker() as mock: - mock.get( - "/stopwords/company_stopwords", - json=json_response, - ) - - response = fake_stopwords_set.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url - == "http://nearest:8108/stopwords/company_stopwords" - ) - assert response == json_response - - -def test_delete(fake_stopwords_set: StopwordsSet) -> None: - """Test that the StopwordsSet object can delete an stopword_set.""" - json_response: StopwordDeleteSchema = { - "id": "company_stopwords", - } - with requests_mock.Mocker() as mock: - mock.delete( - "/stopwords/company_stopwords", - json=json_response, - ) - - response = fake_stopwords_set.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert ( - mock.request_history[0].url - == "http://nearest:8108/stopwords/company_stopwords" - ) - assert response == json_response - - def test_actual_retrieve( actual_stopwords: Stopwords, delete_all_stopwords: None, diff --git a/tests/stopwords_test.py b/tests/stopwords_test.py index a7841d7..cce496d 100644 --- a/tests/stopwords_test.py +++ b/tests/stopwords_test.py @@ -2,7 +2,6 @@ from __future__ import annotations -import requests_mock from tests.utils.object_assertions import ( assert_match_object, @@ -57,59 +56,6 @@ def test_get_existing_stopword(fake_stopwords: Stopwords) -> None: assert stopword is fetched_stopword -def test_retrieve(fake_stopwords: Stopwords) -> None: - """Test that the Stopwords object can retrieve stopwords.""" - json_response: StopwordsRetrieveSchema = { - "stopwords": [ - { - "id": "company_stopwords", - "locale": "", - "stopwords": ["and", "is", "the"], - }, - ], - } - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/stopwords", - json=json_response, - ) - - response = fake_stopwords.retrieve() - - assert len(response) == 1 - assert response["stopwords"][0] == json_response["stopwords"][0] - assert response == json_response - - -def test_create(fake_stopwords: Stopwords) -> None: - """Test that the Stopwords object can create a stopword.""" - json_response: StopwordSchema = { - "id": "company_stopwords", - "locale": "", - "stopwords": ["and", "is", "the"], - } - - with requests_mock.Mocker() as mock: - mock.put( - "http://nearest:8108/stopwords/company_stopwords", - json=json_response, - ) - - fake_stopwords.upsert( - "company_stopwords", - {"stopwords": ["and", "is", "the"]}, - ) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "PUT" - assert ( - mock.last_request.url == "http://nearest:8108/stopwords/company_stopwords" - ) - assert mock.last_request.json() == {"stopwords": ["and", "is", "the"]} - - def test_actual_create(actual_stopwords: Stopwords, delete_all_stopwords: None) -> None: """Test that the Stopwords object can create an stopword on Typesense Server.""" response = actual_stopwords.upsert( diff --git a/tests/synonym_set_items_test.py b/tests/synonym_set_items_test.py index 2cc1dc6..e095d74 100644 --- a/tests/synonym_set_items_test.py +++ b/tests/synonym_set_items_test.py @@ -13,7 +13,6 @@ SynonymItemSchema, ) - pytestmark = pytest.mark.skipif( not is_v30_or_above( Client( @@ -27,55 +26,13 @@ ) -def test_list_items(fake_synonym_set: SynonymSet) -> None: - json_response = [ - {"id": "nike", "synonyms": ["nike", "nikes"]}, - {"id": "adidas", "synonyms": ["adidas", "adi"]}, ] - with requests_mock.Mocker() as mock: - mock.get( - "/synonym_sets/test-set/items?limit=10&offset=0", - json=json_response, - ) - res = fake_synonym_set.list_items(limit=10, offset=0) - assert res == json_response -def test_get_item(fake_synonym_set: SynonymSet) -> None: - json_response: SynonymItemSchema = { - "id": "nike", - "synonyms": ["nike", "nikes"], } - with requests_mock.Mocker() as mock: - mock.get( - "/synonym_sets/test-set/items/nike", - json=json_response, - ) - res = fake_synonym_set.get_item("nike") - assert res == json_response -def test_upsert_item(fake_synonym_set: SynonymSet) -> None: payload: SynonymItemSchema = { - "id": "nike", - "synonyms": ["nike", "nikes"], } - json_response = payload - with requests_mock.Mocker() as mock: - mock.put( - "/synonym_sets/test-set/items/nike", - json=json_response, - ) - res = fake_synonym_set.upsert_item("nike", payload) - assert res == json_response -def test_delete_item(fake_synonym_set: SynonymSet) -> None: - json_response: SynonymItemDeleteSchema = {"id": "nike"} - with requests_mock.Mocker() as mock: - mock.delete( - "/synonym_sets/test-set/items/nike", - json=json_response, - ) - res = fake_synonym_set.delete_item("nike") - assert res == json_response diff --git a/tests/synonym_set_test.py b/tests/synonym_set_test.py index b64aa5c..6e1eaac 100644 --- a/tests/synonym_set_test.py +++ b/tests/synonym_set_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from tests.utils.version import is_v30_or_above @@ -11,8 +10,6 @@ from typesense.client import Client from typesense.synonym_set import SynonymSet from typesense.synonym_sets import SynonymSets -from typesense.types.synonym_set import SynonymSetDeleteSchema, SynonymSetRetrieveSchema - pytestmark = pytest.mark.skipif( not is_v30_or_above( @@ -44,54 +41,6 @@ def test_init(fake_api_call: ApiCall) -> None: assert synset._endpoint_path == "/synonym_sets/test-set" # noqa: WPS437 -def test_retrieve(fake_synonym_set: SynonymSet) -> None: - """Test that the SynonymSet object can retrieve a synonym set.""" - json_response: SynonymSetRetrieveSchema = { - "items": [ - { - "id": "company_synonym", - "synonyms": ["companies", "corporations", "firms"], - } - ] - } - - with requests_mock.Mocker() as mock: - mock.get( - "/synonym_sets/test-set", - json=json_response, - ) - - response = fake_synonym_set.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url == "http://nearest:8108/synonym_sets/test-set" - ) - assert response == json_response - - -def test_delete(fake_synonym_set: SynonymSet) -> None: - """Test that the SynonymSet object can delete a synonym set.""" - json_response: SynonymSetDeleteSchema = { - "name": "test-set", - } - with requests_mock.Mocker() as mock: - mock.delete( - "/synonym_sets/test-set", - json=json_response, - ) - - response = fake_synonym_set.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert ( - mock.request_history[0].url == "http://nearest:8108/synonym_sets/test-set" - ) - assert response == json_response - - def test_actual_retrieve( actual_synonym_sets: SynonymSets, delete_all_synonym_sets: None, diff --git a/tests/synonym_sets_test.py b/tests/synonym_sets_test.py index f63c196..26b5859 100644 --- a/tests/synonym_sets_test.py +++ b/tests/synonym_sets_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import ( assert_match_object, @@ -14,10 +13,6 @@ from typesense.api_call import ApiCall from typesense.client import Client from typesense.synonym_sets import SynonymSets -from typesense.types.synonym_set import ( - SynonymSetCreateSchema, - SynonymSetSchema, -) pytestmark = pytest.mark.skipif( not is_v30_or_above( @@ -47,69 +42,6 @@ def test_init(fake_api_call: ApiCall) -> None: ) -def test_retrieve(fake_synonym_sets: SynonymSets) -> None: - """Test that the SynonymSets object can retrieve synonym sets.""" - json_response = [ - { - "name": "test-set", - "items": [ - { - "id": "company_synonym", - "root": "", - "synonyms": ["companies", "corporations", "firms"], - } - ], - } - ] - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/synonym_sets", - json=json_response, - ) - - response = fake_synonym_sets.retrieve() - - assert isinstance(response, list) - assert len(response) == 1 - assert response == json_response - - -def test_create(fake_synonym_sets: SynonymSets) -> None: - """Test that the SynonymSets object can create a synonym set.""" - json_response: SynonymSetSchema = { - "name": "test-set", - "items": [ - { - "id": "company_synonym", - "synonyms": ["companies", "corporations", "firms"], - } - ], - } - - with requests_mock.Mocker() as mock: - mock.put( - "http://nearest:8108/synonym_sets/test-set", - json=json_response, - ) - - payload: SynonymSetCreateSchema = { - "items": [ - { - "id": "company_synonym", - "synonyms": ["companies", "corporations", "firms"], - } - ] - } - fake_synonym_sets["test-set"].upsert(payload) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "PUT" - assert mock.last_request.url == "http://nearest:8108/synonym_sets/test-set" - assert mock.last_request.json() == payload - - def test_actual_create( actual_synonym_sets: SynonymSets, delete_all_synonym_sets: None, diff --git a/tests/synonym_test.py b/tests/synonym_test.py index 0b2922c..d2d2ada 100644 --- a/tests/synonym_test.py +++ b/tests/synonym_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import ( assert_match_object, @@ -14,8 +13,6 @@ from typesense.api_call import ApiCall from typesense.collections import Collections from typesense.client import Client -from typesense.synonym import Synonym, SynonymDeleteSchema -from typesense.synonyms import SynonymSchema pytestmark = pytest.mark.skipif( @@ -52,52 +49,6 @@ def test_init(fake_api_call: ApiCall) -> None: ) -def test_retrieve(fake_synonym: Synonym) -> None: - """Test that the Synonym object can retrieve an synonym.""" - json_response: SynonymSchema = { - "id": "company_synonym", - "synonyms": ["companies", "corporations", "firms"], - } - - with requests_mock.Mocker() as mock: - mock.get( - "/collections/companies/synonyms/company_synonym", - json=json_response, - ) - - response = fake_synonym.retrieve() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "GET" - assert ( - mock.request_history[0].url - == "http://nearest:8108/collections/companies/synonyms/company_synonym" - ) - assert response == json_response - - -def test_delete(fake_synonym: Synonym) -> None: - """Test that the Synonym object can delete an synonym.""" - json_response: SynonymDeleteSchema = { - "id": "company_synonym", - } - with requests_mock.Mocker() as mock: - mock.delete( - "/collections/companies/synonyms/company_synonym", - json=json_response, - ) - - response = fake_synonym.delete() - - assert len(mock.request_history) == 1 - assert mock.request_history[0].method == "DELETE" - assert ( - mock.request_history[0].url - == "http://nearest:8108/collections/companies/synonyms/company_synonym" - ) - assert response == {"id": "company_synonym"} - - def test_actual_retrieve( actual_collections: Collections, delete_all: None, diff --git a/tests/synonyms_test.py b/tests/synonyms_test.py index 22f8a0c..e8c4b05 100644 --- a/tests/synonyms_test.py +++ b/tests/synonyms_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.object_assertions import ( assert_match_object, @@ -77,63 +76,6 @@ def test_get_existing_synonym(fake_synonyms: Synonyms) -> None: assert synonym is fetched_synonym -def test_retrieve(fake_synonyms: Synonyms) -> None: - """Test that the Synonyms object can retrieve synonyms.""" - json_response: SynonymsRetrieveSchema = { - "synonyms": [ - { - "id": "company_synonym", - "synonyms": ["companies", "corporations", "firms"], - }, - ], - } - - with requests_mock.Mocker() as mock: - mock.get( - "http://nearest:8108/collections/companies/synonyms/", - json=json_response, - ) - - response = fake_synonyms.retrieve() - - assert len(response) == 1 - assert response["synonyms"][0] == { - "id": "company_synonym", - "synonyms": ["companies", "corporations", "firms"], - } - assert response == json_response - - -def test_create(fake_synonyms: Synonyms) -> None: - """Test that the Synonyms object can create a synonym.""" - json_response: SynonymSchema = { - "id": "company_synonym", - "synonyms": ["companies", "corporations", "firms"], - } - - with requests_mock.Mocker() as mock: - mock.put( - "http://nearest:8108/collections/companies/synonyms/company_synonym", - json=json_response, - ) - - fake_synonyms.upsert( - "company_synonym", - {"synonyms": ["companies", "corporations", "firms"]}, - ) - - assert mock.call_count == 1 - assert mock.called is True - assert mock.last_request.method == "PUT" - assert ( - mock.last_request.url - == "http://nearest:8108/collections/companies/synonyms/company_synonym" - ) - assert mock.last_request.json() == { - "synonyms": ["companies", "corporations", "firms"], - } - - def test_actual_create( actual_synonyms: Synonyms, delete_all: None, From d3d4dbe4f93d22eeb12aa2b61b87fbeb27aa6851 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Thu, 4 Dec 2025 16:28:36 +0200 Subject: [PATCH 05/32] test(curation_set): add missing integration tests --- tests/curation_set_test.py | 109 +++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/curation_set_test.py b/tests/curation_set_test.py index f441465..0d60ba2 100644 --- a/tests/curation_set_test.py +++ b/tests/curation_set_test.py @@ -73,3 +73,112 @@ def test_actual_delete( print(response) assert response == {"name": "products"} + + +def test_actual_list_items( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSet object can list items from Typesense Server.""" + response = actual_curation_sets["products"].list_items() + + assert response == [ + { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + }, + "stop_processing": True, + }, + ] + + +def test_actual_get_item( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSet object can get a specific item from Typesense Server.""" + response = actual_curation_sets["products"].get_item("rule-1") + + assert response == { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + }, + "stop_processing": True, + } + + +def test_actual_upsert_item( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSet object can upsert an item in Typesense Server.""" + payload: CurationItemSchema = { + "id": "rule-2", + "rule": {"query": "boot", "match": "exact"}, + "includes": [{"id": "456", "position": 2}], + "excludes": [{"id": "888"}], + } + response = actual_curation_sets["products"].upsert_item("rule-2", payload) + + assert response == { + "excludes": [ + { + "id": "888", + }, + ], + "id": "rule-2", + "includes": [ + { + "id": "456", + "position": 2, + }, + ], + "rule": { + "match": "exact", + "query": "boot", + }, + } + + +def test_actual_delete_item( + actual_curation_sets: CurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the CurationSet object can delete an item from Typesense Server.""" + response = actual_curation_sets["products"].delete_item("rule-1") + + assert response == {"id": "rule-1"} From c5775452660a46cc531396cea52753bc3db2991e Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Thu, 4 Dec 2025 16:28:44 +0200 Subject: [PATCH 06/32] test(synonym_set): add missing integration tests --- tests/synonym_set_items_test.py | 53 +++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tests/synonym_set_items_test.py b/tests/synonym_set_items_test.py index e095d74..0d388a0 100644 --- a/tests/synonym_set_items_test.py +++ b/tests/synonym_set_items_test.py @@ -3,13 +3,11 @@ from __future__ import annotations import pytest -import requests_mock from tests.utils.version import is_v30_or_above from typesense.client import Client -from typesense.synonym_set import SynonymSet +from typesense.synonym_sets import SynonymSets from typesense.types.synonym_set import ( - SynonymItemDeleteSchema, SynonymItemSchema, ) @@ -26,13 +24,62 @@ ) +def test_actual_list_items( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSet object can list items from Typesense Server.""" + response = actual_synonym_sets["test-set"].list_items() + + assert response == [ + { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + }, ] +def test_actual_get_item( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSet object can get a specific item from Typesense Server.""" + response = actual_synonym_sets["test-set"].get_item("company_synonym") + + assert response == { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], } +def test_actual_upsert_item( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSet object can upsert an item in Typesense Server.""" payload: SynonymItemSchema = { + "id": "brand_synonym", + "synonyms": ["brand", "brands", "label"], } + response = actual_synonym_sets["test-set"].upsert_item("brand_synonym", payload) + + assert response == { + "id": "brand_synonym", + "synonyms": ["brand", "brands", "label"], + } + +def test_actual_delete_item( + actual_synonym_sets: SynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the SynonymSet object can delete an item from Typesense Server.""" + response = actual_synonym_sets["test-set"].delete_item("company_synonym") + assert response == {"id": "company_synonym"} From 2f70a2b1956d1db4935c88752d137fdb7afbb086 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:14:33 +0200 Subject: [PATCH 07/32] refactor: migrate request handler from requests to httpx with async support replace requests library with httpx to enable both synchronous and asynchronous http operations. refactor make_request to support sync and async clients with separate implementation methods. - replace requests with httpx for http client operations - add support for both httpx.client and httpx.asyncclient - split make_request into _make_sync_request and _make_async_request - update type hints with bounds for tparams and tbody - change data parameter to content for httpx compatibility - update error handling to use httpx.decodingerror - update documentation to reflect async support --- src/typesense/request_handler.py | 216 ++++++++++++++++++------------- 1 file changed, 127 insertions(+), 89 deletions(-) diff --git a/src/typesense/request_handler.py b/src/typesense/request_handler.py index e726379..38e6c24 100644 --- a/src/typesense/request_handler.py +++ b/src/typesense/request_handler.py @@ -2,12 +2,12 @@ This module provides functionality for handling HTTP requests in the Typesense client library. Classes: - - RequestHandler: Manages HTTP requests to the Typesense API. + - RequestHandler: Manages HTTP requests to the Typesense API (supports both sync and async). - SessionFunctionKwargs: Type for keyword arguments in session functions. The RequestHandler class interacts with the Typesense API to manage HTTP requests, handle authentication, and process responses. It provides methods to send requests, -normalize parameters, and handle errors. +normalize parameters, and handle errors. It supports both sync (httpx.Client) and async (httpx.AsyncClient) clients. This module uses type hinting and is compatible with Python 3.11+ as well as earlier versions through the use of the typing_extensions library. @@ -17,15 +17,16 @@ - Supports JSON and non-JSON responses - Provides custom error handling for various HTTP status codes - Normalizes boolean parameters for API requests +- Supports both sync (httpx.Client) and async (httpx.AsyncClient) HTTP clients -Note: This module relies on the 'requests' library for making HTTP requests. +Note: This module relies on the 'httpx' library for both sync and async operations. """ import json import sys from types import MappingProxyType -import requests +import httpx if sys.version_info >= (3, 11): import typing @@ -47,8 +48,8 @@ ) TEntityDict = typing.TypeVar("TEntityDict") -TParams = typing.TypeVar("TParams") -TBody = typing.TypeVar("TBody") +TParams = typing.TypeVar("TParams", bound=typing.Dict[str, typing.Any]) +TBody = typing.TypeVar("TBody", bound=typing.Union[str, bytes]) _ERROR_CODE_MAP: typing.Mapping[str, typing.Type[TypesenseClientError]] = ( MappingProxyType( @@ -69,33 +70,47 @@ class SessionFunctionKwargs(typing.Generic[TParams, TBody], typing.TypedDict): """ - Type definition for keyword arguments used in session functions. + Type definition for keyword arguments used in request functions. + + This is an internal abstraction that gets converted to httpx's request parameters. + The `data` field is converted to `content` when passed to httpx. + + Note: `verify` and `timeout` are set on the httpx client, not in request kwargs. + However, we include them here for compatibility with the existing API. Attributes: params (Optional[Union[TParams, None]]): Query parameters for the request. + Passed as `params` to httpx. data (Optional[Union[TBody, str, None]]): Body of the request. + Converted to `content` (JSON string) when passed to httpx. headers (Optional[Dict[str, str]]): Headers for the request. + Passed as `headers` to httpx. timeout (float): Timeout for the request in seconds. + Set on the httpx client, not in request kwargs. verify (bool): Whether to verify SSL certificates. + Set on the httpx client, not in request kwargs. """ params: typing.NotRequired[typing.Union[TParams, None]] - data: typing.NotRequired[typing.Union[TBody, str, None]] + data: typing.NotRequired[ + typing.Union[TBody, str, typing.Dict[str, typing.Any], None] + ] + content: typing.NotRequired[typing.Union[TBody, str, None]] headers: typing.NotRequired[typing.Dict[str, str]] - timeout: float - verify: bool + timeout: typing.NotRequired[float] class RequestHandler: """ - Handles HTTP requests to the Typesense API. + Handles HTTP requests to the Typesense API (supports both sync and async using httpx). This class manages authentication, request sending, and response processing - for interactions with the Typesense API. + for interactions with the Typesense API. It can work with both sync (httpx.Client) + and async (httpx.AsyncClient) HTTP clients. Attributes: api_key_header_name (str): The header name for the API key. @@ -113,109 +128,130 @@ def __init__(self, config: Configuration): """ self.config = config - @typing.overload def make_request( self, - fn: typing.Callable[..., requests.models.Response], + *, + method: str, url: str, entity_type: typing.Type[TEntityDict], - as_json: typing.Literal[False], + as_json: typing.Union[typing.Literal[True], typing.Literal[False]] = True, + client: typing.Union[httpx.Client, httpx.AsyncClient], **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], - ) -> str: + ) -> typing.Union[ + TEntityDict, + str, + typing.Coroutine[typing.Any, typing.Any, typing.Union[TEntityDict, str]], + ]: """ - Make an HTTP request to the Typesense API and return the response as a string. - - This overload is used when as_json is set to False, indicating that the response - should be returned as a raw string instead of being parsed as JSON. + Make an HTTP request to the Typesense API (supports both sync and async using httpx). Args: - fn (Callable): The HTTP method function to use (e.g., requests.get). + method (str): The HTTP method (e.g., "GET", "POST", "PUT", "PATCH", "DELETE"). url (str): The URL to send the request to. entity_type (Type[TEntityDict]): The expected type of the response entity. - as_json (Literal[False]): Specifies that the response should not be parsed as JSON. + as_json (bool): Whether to return the response as JSON. Defaults to True. + + client: The httpx client to use (httpx.Client for sync, httpx.AsyncClient for async). kwargs: Additional keyword arguments for the request. Returns: - str: The raw string response from the API. + Union[TEntityDict, str]: The response, either as a JSON object or a string. + If using AsyncClient, returns a coroutine. Raises: TypesenseClientError: If the API returns an error response. """ + headers = { + self.api_key_header_name: self.config.api_key, + } + headers.update(self.config.additional_headers) - @typing.overload - def make_request( + request_kwargs: SessionFunctionKwargs[TParams, TBody] = typing.cast( + SessionFunctionKwargs[TParams, TBody], + { + "headers": headers, + "timeout": self.config.connection_timeout_seconds, + }, + ) + + if params := kwargs.get("params"): + self.normalize_params(params) + request_kwargs["params"] = params + + if body := kwargs.get("data"): + if not isinstance(body, (str, bytes)): + body = json.dumps(body) + request_kwargs["content"] = typing.cast(TBody, body) + + if isinstance(client, httpx.AsyncClient): + return self._make_async_request( + method, url, entity_type, as_json, client, **request_kwargs + ) + else: + return self._make_sync_request( + method, url, entity_type, as_json, client, **request_kwargs + ) + + def _make_sync_request( self, - fn: typing.Callable[..., requests.models.Response], + method: str, url: str, entity_type: typing.Type[TEntityDict], - as_json: typing.Literal[True], - **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], - ) -> TEntityDict: - """ - Make an HTTP request to the Typesense API. - - Args: - fn (Callable): The HTTP method function to use (e.g., requests.get). - - url (str): The URL to send the request to. - - entity_type (Type[TEntityDict]): The expected type of the response entity. - - as_json (bool): Whether to return the response as JSON. Defaults to True. + as_json: bool, + client: httpx.Client, + **request_kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], + ) -> typing.Union[TEntityDict, str]: + """Make a synchronous HTTP request using httpx.Client.""" + params: typing.Union[TParams, None] = request_kwargs.get("params") + content: typing.Union[TBody, str, None] = request_kwargs.get("content") + headers: typing.Dict[str, str] = request_kwargs.get("headers", {}) + + response = client.request( + method, + url, + params=params, + content=content, + headers=headers, + ) - kwargs: Additional keyword arguments for the request. + if response.status_code < 200 or response.status_code >= 300: + error_message = self._get_error_message(response) + raise self._get_exception(response.status_code)( + response.status_code, + error_message, + ) - Returns: - TEntityDict: The response, as a JSON object. + if as_json: + res: TEntityDict = typing.cast(TEntityDict, response.json()) + return res - Raises: - TypesenseClientError: If the API returns an error response. - """ + return response.text - def make_request( + async def _make_async_request( self, - fn: typing.Callable[..., requests.models.Response], + method: str, url: str, entity_type: typing.Type[TEntityDict], - as_json: typing.Union[typing.Literal[True], typing.Literal[False]] = True, - **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], + as_json: bool, + client: httpx.AsyncClient, + **request_kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], ) -> typing.Union[TEntityDict, str]: - """ - Make an HTTP request to the Typesense API. - - Args: - fn (Callable): The HTTP method function to use (e.g., requests.get). - - url (str): The URL to send the request to. - - entity_type (Type[TEntityDict]): The expected type of the response entity. - - as_json (bool): Whether to return the response as JSON. Defaults to True. - - kwargs: Additional keyword arguments for the request. - - Returns: - Union[TEntityDict, str]: The response, either as a JSON object or a string. - - Raises: - TypesenseClientError: If the API returns an error response. - """ - headers = { - self.api_key_header_name: self.config.api_key, - } - headers.update(self.config.additional_headers) - - kwargs.setdefault("headers", {}).update(headers) - kwargs.setdefault("timeout", self.config.connection_timeout_seconds) - kwargs.setdefault("verify", self.config.verify) - if kwargs.get("data") and not isinstance(kwargs["data"], (str, bytes)): - kwargs["data"] = json.dumps(kwargs["data"]) - - response = fn(url, **kwargs) + """Make an asynchronous HTTP request using httpx.AsyncClient.""" + params: typing.Union[TParams, None] = request_kwargs.get("params") + content: typing.Union[TBody, str, None] = request_kwargs.get("content") + headers: typing.Dict[str, str] = request_kwargs.get("headers", {}) + + response = await client.request( + method, + url, + params=params, + content=content, + headers=headers, + ) if response.status_code < 200 or response.status_code >= 300: error_message = self._get_error_message(response) @@ -225,18 +261,18 @@ def make_request( ) if as_json: - res: TEntityDict = response.json() + res: TEntityDict = typing.cast(TEntityDict, response.json()) return res return response.text @staticmethod - def normalize_params(params: TParams) -> None: + def normalize_params(params: typing.Dict[str, typing.Any]) -> None: """ Normalize boolean parameters in the request. Args: - params (TParams): The parameters to normalize. + params (Dict[str, Any]): The parameters to normalize. Raises: ValueError: If params is not a dictionary. @@ -248,12 +284,12 @@ def normalize_params(params: TParams) -> None: params[key] = str(parameter_value).lower() @staticmethod - def _get_error_message(response: requests.Response) -> str: + def _get_error_message(response: httpx.Response) -> str: """ Extract the error message from an API response. Args: - response (requests.Response): The API response. + response (httpx.Response): The API response. Returns: str: The extracted error message or a default message. @@ -262,9 +298,11 @@ def _get_error_message(response: requests.Response) -> str: if content_type.startswith("application/json"): try: return typing.cast(str, response.json().get("message", "API error.")) - except requests.exceptions.JSONDecodeError: + except (json.JSONDecodeError, httpx.DecodingError): return f"API error: Invalid JSON response: {response.text}" - return "API error." + if response.text: + return f"API error. {response.text}" + return f"Unknown API error. Full Response: {response}" @staticmethod def _get_exception(http_code: int) -> typing.Type[TypesenseClientError]: From fb8780d8fe0a931f1253a659eb3560a0493db78b Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:36:49 +0200 Subject: [PATCH 08/32] chore: update test dependencies for httpx migration - replace requests-mock with respx for httpx test mocking - add pytest-asyncio configuration with auto mode - remove types-requests dependency - update charset-normalizer, requests, and urllib3 versions --- pyproject.toml | 4 +- pytest.ini | 1 + uv.lock | 259 +++++++++++++++++++++++++++++++------------------ 3 files changed, 166 insertions(+), 98 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a5e0f82..2e169c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,13 +39,15 @@ build-backend = "setuptools.build_meta" dev = [ "mypy>=1.19.0", "pytest", + "pytest-asyncio", "coverage", "pytest-mock", "python-dotenv", - "types-requests", "faker", "ruff>=0.11.11", "isort>=6.0.1", + "respx>=0.22.0", + "requests", ] [tool.uv] diff --git a/pytest.ini b/pytest.ini index fd1accd..c7da18e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,5 @@ [pytest] pythonpath = src +asyncio_mode = auto markers = open_ai diff --git a/uv.lock b/uv.lock index d7d7d0e..16132b1 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] +[[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 = "2025.4.26" @@ -31,76 +40,107 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, - { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, - { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, - { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, - { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, - { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] @@ -462,6 +502,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10'" }, + { name = "pytest", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*'" }, + { name = "pytest", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + [[package]] name = "pytest-mock" version = "3.14.1" @@ -485,7 +559,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.3" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -493,21 +567,21 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] -name = "requests-mock" -version = "1.12.1" +name = "respx" +version = "0.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, + { name = "httpx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, ] [[package]] @@ -574,18 +648,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] -[[package]] -name = "types-requests" -version = "2.32.0.20250515" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c1/cdc4f9b8cfd9130fbe6276db574f114541f4231fcc6fb29648289e6e3390/types_requests-2.32.0.20250515.tar.gz", hash = "sha256:09c8b63c11318cb2460813871aaa48b671002e59fda67ca909e9883777787581", size = 23012, upload-time = "2025-05-15T03:04:31.817Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/0f/68a997c73a129287785f418c1ebb6004f81e46b53b3caba88c0e03fcd04a/types_requests-2.32.0.20250515-py3-none-any.whl", hash = "sha256:f8eba93b3a892beee32643ff836993f15a785816acca21ea0ffa006f05ef0fb2", size = 20635, upload-time = "2025-05-15T03:04:30.5Z" }, -] - [[package]] name = "typesense" source = { virtual = "." } @@ -601,11 +663,13 @@ dev = [ { name = "isort" }, { name = "mypy" }, { name = "pytest" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-mock" }, { name = "python-dotenv" }, - { name = "requests-mock" }, + { name = "requests" }, + { name = "respx" }, { name = "ruff" }, - { name = "types-requests" }, ] [package.metadata] @@ -621,11 +685,12 @@ dev = [ { name = "isort", specifier = ">=6.0.1" }, { name = "mypy", specifier = ">=1.19.0" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-mock" }, { name = "python-dotenv" }, - { name = "requests-mock" }, + { name = "requests" }, + { name = "respx", specifier = ">=0.22.0" }, { name = "ruff", specifier = ">=0.11.11" }, - { name = "types-requests" }, ] [[package]] @@ -648,9 +713,9 @@ wheels = [ [[package]] name = "urllib3" -version = "2.4.0" +version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/1d/0f3a93cca1ac5e8287842ed4eebbd0f7a991315089b1a0b01c7788aa7b63/urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f", size = 432678, upload-time = "2025-12-08T15:25:26.773Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b", size = 131138, upload-time = "2025-12-08T15:25:25.51Z" }, ] From 9c2e2ae3ce36380a8bc445c12305ebe831e5f044 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:33:59 +0200 Subject: [PATCH 09/32] refactor: migrate api_call from requests to httpx and add async support - replace requests with httpx in api_call class - add async_api_call class with httpx.asyncclient support - migrate tests from requests_mock to respx --- src/typesense/api_call.py | 194 ++++++++-- src/typesense/async_api_call.py | 541 ++++++++++++++++++++++++++++ tests/api_call_test.py | 289 +++++++-------- tests/fixtures/api_call_fixtures.py | 16 + 4 files changed, 852 insertions(+), 188 deletions(-) create mode 100644 src/typesense/async_api_call.py diff --git a/src/typesense/api_call.py b/src/typesense/api_call.py index 90e1929..efcd26c 100644 --- a/src/typesense/api_call.py +++ b/src/typesense/api_call.py @@ -34,8 +34,12 @@ import sys -import requests +import httpx +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing from typesense.configuration import Configuration, Node from typesense.exceptions import ( HTTPStatus0Error, @@ -44,36 +48,138 @@ TypesenseClientError, ) from typesense.node_manager import NodeManager -from typesense.request_handler import RequestHandler, SessionFunctionKwargs +from typesense.request_handler import ( + RequestHandler, +) + +TEntityDict = typing.TypeVar("TEntityDict") +TParams = typing.TypeVar("TParams", bound=typing.Dict[str, typing.Any]) +TBody = typing.TypeVar( + "TBody", bound=typing.Union[str, bytes, typing.Mapping[str, typing.Any]] +) + + +class SessionFunctionKwargs(typing.Generic[TParams, TBody], typing.TypedDict): + """ + Type definition for keyword arguments used in request functions. + + This is an internal abstraction that gets converted to httpx's request parameters. + The `data` field is converted to `content` when passed to httpx. + + Note: `verify` and `timeout` are set on the httpx client, not in request kwargs. + However, we include them here for compatibility with the existing API. + + Attributes: + params (Optional[Union[TParams, None]]): Query parameters for the request. + Passed as `params` to httpx. + + data (Optional[Union[TBody, str, None]]): Body of the request. + Converted to `content` (JSON string) when passed to httpx. + + headers (Optional[Dict[str, str]]): Headers for the request. + Passed as `headers` to httpx. + + timeout (float): Timeout for the request in seconds. + Set on the httpx client, not in request kwargs. + + verify (bool): Whether to verify SSL certificates. + Set on the httpx client, not in request kwargs. + """ + + params: typing.NotRequired[typing.Union[TParams, None]] + data: typing.NotRequired[ + typing.Union[TBody, str, typing.Dict[str, typing.Any], None] + ] + content: typing.NotRequired[typing.Union[TBody, str, None]] + headers: typing.NotRequired[typing.Dict[str, str]] + timeout: typing.NotRequired[float] + if sys.version_info >= (3, 11): import typing else: import typing_extensions as typing -session = requests.sessions.Session() -TParams = typing.TypeVar("TParams") -TBody = typing.TypeVar("TBody") -TEntityDict = typing.TypeVar("TEntityDict") + +class ApiCallProtocol(typing.Protocol): + """ + Protocol defining the interface for API call classes. + + This protocol ensures that both sync (ApiCall) and async (AsyncApiCall) + implementations provide the same interface, allowing resource classes + to work with either implementation. + """ + + config: Configuration + node_manager: NodeManager + request_handler: RequestHandler + + def get( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Union[typing.Literal[True], typing.Literal[False]] = True, + params: typing.Union[TParams, None] = None, + ) -> typing.Union[TEntityDict, str]: + """Execute a GET request to the Typesense API.""" + ... + + def post( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Union[typing.Literal[True], typing.Literal[False]] = True, + params: typing.Union[TParams, None] = None, + body: typing.Union[TBody, None] = None, + ) -> typing.Union[str, TEntityDict]: + """Execute a POST request to the Typesense API.""" + ... + + def put( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + body: TBody, + params: typing.Union[TParams, None] = None, + ) -> TEntityDict: + """Execute a PUT request to the Typesense API.""" + ... + + def patch( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + body: TBody, + params: typing.Union[TParams, None] = None, + ) -> TEntityDict: + """Execute a PATCH request to the Typesense API.""" + ... + + def delete( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + params: typing.Union[TParams, None] = None, + ) -> TEntityDict: + """Execute a DELETE request to the Typesense API.""" + ... _SERVER_ERRORS: typing.Final[ typing.Tuple[ - typing.Type[requests.exceptions.Timeout], - typing.Type[requests.exceptions.ConnectionError], - typing.Type[requests.exceptions.HTTPError], - typing.Type[requests.exceptions.RequestException], - typing.Type[requests.exceptions.SSLError], + typing.Type[httpx.TimeoutException], + typing.Type[httpx.ConnectError], + typing.Type[httpx.HTTPError], + typing.Type[httpx.RequestError], typing.Type[HTTPStatus0Error], typing.Type[ServerError], typing.Type[ServiceUnavailable], ] ] = ( - requests.exceptions.Timeout, - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - requests.exceptions.RequestException, - requests.exceptions.SSLError, + httpx.TimeoutException, + httpx.ConnectError, + httpx.HTTPError, + httpx.RequestError, HTTPStatus0Error, ServerError, ServiceUnavailable, @@ -103,6 +209,10 @@ def __init__(self, config: Configuration): self.config = config self.node_manager = NodeManager(config) self.request_handler = RequestHandler(config) + self._client = httpx.Client( + timeout=config.connection_timeout_seconds, + verify=config.verify, + ) @typing.overload def get( @@ -166,7 +276,7 @@ def get( Union[TEntityDict, str]: The response, either as a JSON object or a string. """ return self._execute_request( - session.get, + "GET", endpoint, entity_type, as_json, @@ -238,7 +348,7 @@ def post( Union[TEntityDict, str]: The response, either as a JSON object or a string. """ return self._execute_request( - session.post, + "POST", endpoint, entity_type, as_json, @@ -265,7 +375,7 @@ def put( EntityDict: The response, as a JSON object. """ return self._execute_request( - session.put, + "PUT", endpoint, entity_type, as_json=True, @@ -292,7 +402,7 @@ def patch( EntityDict: The response, as a JSON object. """ return self._execute_request( - session.patch, + "PATCH", endpoint, entity_type, as_json=True, @@ -318,7 +428,7 @@ def delete( EntityDict: The response, as a JSON object. """ return self._execute_request( - session.delete, + "DELETE", endpoint, entity_type, as_json=True, @@ -328,13 +438,13 @@ def delete( @typing.overload def _execute_request( self, - fn: typing.Callable[..., requests.models.Response], + method: str, endpoint: str, entity_type: typing.Type[TEntityDict], as_json: typing.Literal[True], last_exception: typing.Union[None, Exception] = None, num_retries: int = 0, - **kwargs: SessionFunctionKwargs[TParams, TBody], + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], ) -> TEntityDict: """ Execute a request to the Typesense API with retry logic. @@ -367,13 +477,13 @@ def _execute_request( @typing.overload def _execute_request( self, - fn: typing.Callable[..., requests.models.Response], + method: str, endpoint: str, entity_type: typing.Type[TEntityDict], as_json: typing.Literal[False], last_exception: typing.Union[None, Exception] = None, num_retries: int = 0, - **kwargs: SessionFunctionKwargs[TParams, TBody], + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], ) -> str: """ Execute a request to the Typesense API with retry logic. @@ -405,13 +515,13 @@ def _execute_request( def _execute_request( self, - fn: typing.Callable[..., requests.models.Response], + method: str, endpoint: str, entity_type: typing.Type[TEntityDict], as_json: typing.Union[typing.Literal[True], typing.Literal[False]] = True, last_exception: typing.Union[None, Exception] = None, num_retries: int = 0, - **kwargs: SessionFunctionKwargs[TParams, TBody], + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], ) -> typing.Union[TEntityDict, str]: """ Execute a request to the Typesense API with retry logic. @@ -420,7 +530,7 @@ def _execute_request( node selection, error handling, and retries. Args: - fn (Callable): The HTTP method function to use (e.g., session.get). + method (str): The HTTP method to use (e.g., "GET", "POST"). endpoint (str): The API endpoint to call. @@ -449,7 +559,7 @@ def _execute_request( try: return self._make_request_and_process_response( - fn, + method, url, entity_type, as_json, @@ -458,7 +568,7 @@ def _execute_request( except _SERVER_ERRORS as server_error: self.node_manager.set_node_health(node, is_healthy=False) return self._execute_request( - fn, + method, endpoint, entity_type, as_json, @@ -469,18 +579,19 @@ def _execute_request( def _make_request_and_process_response( self, - fn: typing.Callable[..., requests.models.Response], + method: str, url: str, entity_type: typing.Type[TEntityDict], as_json: bool, - **kwargs: SessionFunctionKwargs[TParams, TBody], + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], ) -> typing.Union[TEntityDict, str]: """Make the API request and process the response.""" request_response = self.request_handler.make_request( - fn=fn, + method=method, url=url, as_json=as_json, entity_type=entity_type, + client=self._client, **kwargs, ) self.node_manager.set_node_health(self.node_manager.get_node(), is_healthy=True) @@ -493,12 +604,23 @@ def _make_request_and_process_response( def _prepare_request_params( self, endpoint: str, - **kwargs: SessionFunctionKwargs[TParams, TBody], + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], ) -> typing.Tuple[Node, str, SessionFunctionKwargs[TParams, TBody]]: + """ + Prepare request parameters including node selection and URL construction. + + Args: + endpoint: The API endpoint path. + **kwargs: Request parameters following SessionFunctionKwargs structure. + + Returns: + Tuple of (node, full_url, kwargs_dict) where kwargs_dict contains + the request parameters as a regular dict for further processing. + """ node = self.node_manager.get_node() url = node.url() + endpoint - if kwargs.get("params"): - self.request_handler.normalize_params(kwargs["params"]) + if params := kwargs.get("params"): + self.request_handler.normalize_params(params) return node, url, kwargs diff --git a/src/typesense/async_api_call.py b/src/typesense/async_api_call.py new file mode 100644 index 0000000..a78e3cc --- /dev/null +++ b/src/typesense/async_api_call.py @@ -0,0 +1,541 @@ +""" +This module provides async functionality for making API calls to a Typesense server. + +It contains the AsyncApiCall class, which is responsible for executing async HTTP requests +to the Typesense API, handling retries, and managing node health. + +Key features: +- Support for GET, POST, PUT, PATCH, and DELETE HTTP methods (async) +- Automatic retries on server errors +- Node health management +- Type-safe request execution with overloaded methods + +Classes: + AsyncApiCall: Manages async API calls to the Typesense server. + +Dependencies: + - httpx: For making async HTTP requests + - typesense.configuration: Provides Configuration and Node classes + - typesense.exceptions: Custom exception classes + - typesense.node_manager: Provides NodeManager class + +Usage: + from typesense.configuration import Configuration + from typesense.async_api_call import AsyncApiCall + + config = Configuration(...) + api_call = AsyncApiCall(config) + response = await api_call.get("/collections", SomeEntityType) + +Note: This module is part of the Typesense Python client library and is used internally +by other components of the library. +""" + +import sys +from types import MappingProxyType, TracebackType + +import httpx + +from typesense.configuration import Configuration, Node +from typesense.exceptions import ( + HTTPStatus0Error, + ObjectAlreadyExists, + ObjectNotFound, + ObjectUnprocessable, + RequestForbidden, + RequestMalformed, + RequestUnauthorized, + ServerError, + ServiceUnavailable, + TypesenseClientError, +) +from typesense.node_manager import NodeManager +from typesense.request_handler import RequestHandler + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +TEntityDict = typing.TypeVar("TEntityDict") +TParams = typing.TypeVar("TParams", bound=typing.Dict[str, typing.Any]) +TBody = typing.TypeVar( + "TBody", bound=typing.Union[str, bytes, typing.Mapping[str, typing.Any]] +) + + +class SessionFunctionKwargs(typing.Generic[TParams, TBody], typing.TypedDict): + """ + Type definition for keyword arguments used in request functions. + + This is an internal abstraction that gets converted to httpx's request parameters. + The `data` field is converted to `content` when passed to httpx. + + Note: `verify` and `timeout` are set on the httpx client, not in request kwargs. + However, we include them here for compatibility with the existing API. + + Attributes: + params (Optional[Union[TParams, None]]): Query parameters for the request. + Passed as `params` to httpx. + + data (Optional[Union[TBody, str, None]]): Body of the request. + Converted to `content` (JSON string) when passed to httpx. + + headers (Optional[Dict[str, str]]): Headers for the request. + Passed as `headers` to httpx. + + timeout (float): Timeout for the request in seconds. + Set on the httpx client, not in request kwargs. + + verify (bool): Whether to verify SSL certificates. + Set on the httpx client, not in request kwargs. + """ + + params: typing.NotRequired[typing.Union[TParams, None]] + data: typing.NotRequired[typing.Union[TBody, None]] + content: typing.NotRequired[typing.Union[TBody, str, None]] + headers: typing.NotRequired[typing.Dict[str, str]] + timeout: typing.NotRequired[float] + + +_ERROR_CODE_MAP: typing.Final[ + typing.Mapping[str, typing.Type[TypesenseClientError]] +] = MappingProxyType( + { + "0": HTTPStatus0Error, + "400": RequestMalformed, + "401": RequestUnauthorized, + "403": RequestForbidden, + "404": ObjectNotFound, + "409": ObjectAlreadyExists, + "422": ObjectUnprocessable, + "500": ServerError, + "503": ServiceUnavailable, + }, +) + +_SERVER_ERRORS: typing.Final[ + typing.Tuple[ + typing.Type[httpx.TimeoutException], + typing.Type[httpx.ConnectError], + typing.Type[httpx.HTTPError], + typing.Type[httpx.RequestError], + typing.Type[HTTPStatus0Error], + typing.Type[ServerError], + typing.Type[ServiceUnavailable], + ] +] = ( + httpx.TimeoutException, + httpx.ConnectError, + httpx.HTTPError, + httpx.RequestError, + HTTPStatus0Error, + ServerError, + ServiceUnavailable, +) + + +class AsyncApiCall: + """ + Manages async API calls to the Typesense server. + + This class handles the execution of async HTTP requests to the Typesense API, + including retries, node health management, and error handling. + + Attributes: + config (Configuration): The configuration object for the Typesense client. + node_manager (NodeManager): Manages the nodes in the Typesense cluster. + _client (httpx.AsyncClient): The httpx async client for making requests. + """ + + def __init__(self, config: Configuration): + """ + Initialize the AsyncApiCall instance. + + Args: + config (Configuration): The configuration object for the Typesense client. + """ + self.config = config + self.node_manager = NodeManager(config) + self.request_handler = RequestHandler(config) + self._client = httpx.AsyncClient( + timeout=config.connection_timeout_seconds, + ) + + async def __aenter__(self) -> "AsyncApiCall": + """Async context manager entry.""" + return self + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[TracebackType], + ) -> None: + """Async context manager exit.""" + await self._client.aclose() + + async def aclose(self) -> None: + """Close the httpx client.""" + await self._client.aclose() + + @typing.overload + async def get( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Literal[False], + params: typing.Union[TParams, None] = None, + ) -> str: + """ + Execute an async GET request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + as_json (False): Whether to return the response as JSON. Defaults to True. + params (Union[TParams, None], optional): Query parameters for the request. + + Returns: + str: The response, as a string. + """ + + @typing.overload + async def get( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Literal[True], + params: typing.Union[TParams, None] = None, + ) -> TEntityDict: + """ + Execute an async GET request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + as_json (True): Whether to return the response as JSON. Defaults to True. + params (Union[TParams, None], optional): Query parameters for the request. + + Returns: + EntityDict: The response, as a JSON object. + """ + + async def get( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Union[typing.Literal[True], typing.Literal[False]] = True, + params: typing.Union[TParams, None] = None, + ) -> typing.Union[TEntityDict, str]: + """ + Execute an async GET request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + as_json (bool): Whether to return the response as JSON. Defaults to True. + params (Union[TParams, None], optional): Query parameters for the request. + + Returns: + Union[TEntityDict, str]: The response, either as a JSON object or a string. + """ + return await self._execute_request( + "GET", + endpoint, + entity_type, + as_json, + params=params, + ) + + @typing.overload + async def post( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Literal[False], + params: typing.Union[TParams, None] = None, + body: typing.Union[TBody, None] = None, + ) -> str: + """ + Execute an async POST request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + as_json (False): Whether to return the response as JSON. Defaults to True. + params (Union[TParams, None], optional): Query parameters for the request. + body (Union[TBody, None], optional): Request body. + + Returns: + str: The response, as a string. + """ + + @typing.overload + async def post( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Literal[True], + params: typing.Union[TParams, None] = None, + body: typing.Union[TBody, None] = None, + ) -> TEntityDict: + """ + Execute an async POST request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + as_json (True): Whether to return the response as JSON. Defaults to True. + params (Union[TParams, None], optional): Query parameters for the request. + body (Union[TBody, None], optional): Request body. + + Returns: + EntityDict: The response, as a JSON object. + """ + + async def post( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Union[typing.Literal[True], typing.Literal[False]] = True, + params: typing.Union[TParams, None] = None, + body: typing.Union[TBody, None] = None, + ) -> typing.Union[str, TEntityDict]: + """ + Execute an async POST request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + as_json (bool): Whether to return the response as JSON. Defaults to True. + params (Union[TParams, None], optional): Query parameters for the request. + body (Union[TBody, None], optional): Request body. + + Returns: + Union[TEntityDict, str]: The response, either as a JSON object or a string. + """ + return await self._execute_request( + "POST", + endpoint, + entity_type, + as_json, + params=params, + data=body, + ) + + async def put( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + body: TBody, + params: typing.Union[TParams, None] = None, + ) -> TEntityDict: + """ + Execute an async PUT request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + params (Union[TParams, None], optional): Query parameters for the request. + body (TBody): Request body. + + Returns: + EntityDict: The response, as a JSON object. + """ + return await self._execute_request( + "PUT", + endpoint, + entity_type, + as_json=True, + params=params, + data=body, + ) + + async def patch( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + body: TBody, + params: typing.Union[TParams, None] = None, + ) -> TEntityDict: + """ + Execute an async PATCH request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + params (Union[TParams, None], optional): Query parameters for the request. + body (TBody): Request body. + + Returns: + EntityDict: The response, as a JSON object. + """ + return await self._execute_request( + "PATCH", + endpoint, + entity_type, + as_json=True, + params=params, + data=body, + ) + + async def delete( + self, + endpoint: str, + entity_type: typing.Type[TEntityDict], + params: typing.Union[TParams, None] = None, + ) -> TEntityDict: + """ + Execute an async DELETE request to the Typesense API. + + Args: + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + params (Union[TParams, None], optional): Query parameters for the request. + + Returns: + EntityDict: The response, as a JSON object. + """ + return await self._execute_request( + "DELETE", + endpoint, + entity_type, + as_json=True, + params=params, + ) + + @typing.overload + async def _execute_request( + self, + method: str, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Literal[True], + last_exception: typing.Union[None, Exception] = None, + num_retries: int = 0, + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], + ) -> TEntityDict: + """Execute an async request with retry logic.""" + + @typing.overload + async def _execute_request( + self, + method: str, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Literal[False], + last_exception: typing.Union[None, Exception] = None, + num_retries: int = 0, + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], + ) -> str: + """Execute an async request with retry logic.""" + + async def _execute_request( + self, + method: str, + endpoint: str, + entity_type: typing.Type[TEntityDict], + as_json: typing.Union[typing.Literal[True], typing.Literal[False]] = True, + last_exception: typing.Union[None, Exception] = None, + num_retries: int = 0, + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], + ) -> typing.Union[TEntityDict, str]: + """ + Execute an async request to the Typesense API with retry logic. + + This method handles the actual execution of the request, including + node selection, error handling, and retries. + + Args: + method (str): The HTTP method to use (GET, POST, PUT, PATCH, DELETE). + endpoint (str): The API endpoint to call. + entity_type (Type[TEntityDict]): The expected type of the response entity. + as_json (bool): Whether to return the response as JSON. Defaults to True. + last_exception (Union[None, Exception], optional): The last exception encountered. + num_retries (int): The current number of retries attempted. + kwargs: Additional keyword arguments for the request. + + Returns: + Union[TEntityDict, str]: The response, either as a JSON object or a string. + + Raises: + TypesenseClientError: If all nodes are unhealthy or max retries are exceeded. + """ + if num_retries > self.config.num_retries: + if last_exception: + raise last_exception + raise TypesenseClientError("All nodes are unhealthy") + + node, url, request_kwargs = self._prepare_request_params(endpoint, **kwargs) + + try: + return await self._make_request_and_process_response( + method, + url, + entity_type, + as_json, + **request_kwargs, + ) + except _SERVER_ERRORS as server_error: + self.node_manager.set_node_health(node, is_healthy=False) + return await self._execute_request( + method, + endpoint, + entity_type, + as_json, + last_exception=server_error, + num_retries=num_retries + 1, + **kwargs, + ) + + async def _make_request_and_process_response( + self, + method: str, + url: str, + entity_type: typing.Type[TEntityDict], + as_json: bool, + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], + ) -> typing.Union[TEntityDict, str]: + """Make the async API request and process the response.""" + request_response = await self.request_handler.make_request( + method=method, + url=url, + as_json=as_json, + entity_type=entity_type, + client=self._client, + **kwargs, + ) + self.node_manager.set_node_health( + self.node_manager.get_node(), + is_healthy=True, + ) + return ( + typing.cast(TEntityDict, request_response) + if as_json + else typing.cast(str, request_response) + ) + + def _prepare_request_params( + self, + endpoint: str, + **kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]], + ) -> typing.Tuple[Node, str, SessionFunctionKwargs[TParams, TBody]]: + """ + Prepare request parameters including node selection and URL construction. + + Args: + endpoint: The API endpoint path. + **kwargs: Request parameters following SessionFunctionKwargs structure. + + Returns: + Tuple of (node, full_url, kwargs_dict) where kwargs_dict contains + the request parameters as a regular dict for further processing. + """ + node = self.node_manager.get_node() + url = node.url() + endpoint + + if params := kwargs.get("params"): + self.request_handler.normalize_params(params) + + return node, url, kwargs diff --git a/tests/api_call_test.py b/tests/api_call_test.py index 96acadf..b5304ab 100644 --- a/tests/api_call_test.py +++ b/tests/api_call_test.py @@ -1,7 +1,5 @@ """Unit Tests for the ApiCall class.""" -from __future__ import annotations - import logging import sys import time @@ -13,9 +11,9 @@ else: import typing_extensions as typing +import httpx import pytest -import requests -import requests_mock +import respx from pytest_mock import MockerFixture from tests.utils.object_assertions import assert_match_object, assert_object_lists_match @@ -95,11 +93,11 @@ def test_get_exception() -> None: def test_get_error_message_with_invalid_json() -> None: """Test that it correctly handles invalid JSON in error responses.""" - response = requests.Response() - response.headers["Content-Type"] = "application/json" - response.status_code = 400 - # Set an invalid JSON string that would cause JSONDecodeError - response._content = b'{"message": "Error occurred", "details": {"key": "value"' + response = httpx.Response( + 400, + headers={"Content-Type": "application/json"}, + content=b'{"message": "Error occurred", "details": {"key": "value"', + ) error_message = RequestHandler._get_error_message(response) assert "API error: Invalid JSON response:" in error_message @@ -108,10 +106,11 @@ def test_get_error_message_with_invalid_json() -> None: def test_get_error_message_with_valid_json() -> None: """Test that it correctly extracts error message from valid JSON responses.""" - response = requests.Response() - response.headers["Content-Type"] = "application/json" - response.status_code = 400 - response._content = b'{"message": "Error occurred", "details": {"key": "value"}}' + response = httpx.Response( + 400, + headers={"Content-Type": "application/json"}, + content=b'{"message": "Error occurred", "details": {"key": "value"}}', + ) error_message = RequestHandler._get_error_message(response) assert error_message == "Error occurred" @@ -119,13 +118,14 @@ def test_get_error_message_with_valid_json() -> None: def test_get_error_message_with_non_json_content_type() -> None: """Test that it returns a default error message for non-JSON content types.""" - response = requests.Response() - response.headers["Content-Type"] = "text/plain" - response.status_code = 400 - response._content = b"Not a JSON content" + response = httpx.Response( + 400, + headers={"Content-Type": "text/plain"}, + content=b"Not a JSON content", + ) error_message = RequestHandler._get_error_message(response) - assert error_message == "API error." + assert error_message == "API error. Not a JSON content" def test_normalize_params_with_booleans() -> None: @@ -172,7 +172,6 @@ def test_normalize_params_with_no_booleans() -> None: def test_additional_headers(fake_api_call: ApiCall) -> None: """Test the `make_request` method with additional headers from the config.""" - session = requests.sessions.Session() api_call = ApiCall( Configuration( { @@ -188,38 +187,32 @@ def test_additional_headers(fake_api_call: ApiCall) -> None: ), ) - with requests_mock.mock(session=session) as request_mocker: - request_mocker.get( - "http://nearest:8108/test", - json={"key": "value"}, - status_code=200, + with respx.mock: + respx.get("http://nearest:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) api_call._execute_request( - session.get, + "GET", "/test", as_json=True, entity_type=typing.Dict[str, str], ) - request = request_mocker.request_history[-1] + request = respx.calls.last.request assert request.headers["AdditionalHeader1"] == "test" assert request.headers["AdditionalHeader2"] == "test2" def test_make_request_as_json(fake_api_call: ApiCall) -> None: """Test the `make_request` method with JSON response.""" - session = requests.sessions.Session() - - with requests_mock.mock(session=session) as request_mocker: - request_mocker.get( - "http://nearest:8108/test", - json={"key": "value"}, - status_code=200, + with respx.mock: + respx.get("http://nearest:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) response = fake_api_call._execute_request( - session.get, + "GET", "/test", as_json=True, entity_type=typing.Dict[str, str], @@ -229,17 +222,13 @@ def test_make_request_as_json(fake_api_call: ApiCall) -> None: def test_make_request_as_text(fake_api_call: ApiCall) -> None: """Test the `make_request` method with text response.""" - session = requests.sessions.Session() - - with requests_mock.mock(session=session) as request_mocker: - request_mocker.get( - "http://nearest:8108/test", - text="response text", - status_code=200, + with respx.mock: + respx.get("http://nearest:8108/test").mock( + return_value=httpx.Response(200, text="response text") ) response = fake_api_call._execute_request( - session.get, + "GET", "/test", as_json=False, entity_type=typing.Dict[str, str], @@ -252,11 +241,9 @@ def test_get_as_json( fake_api_call: ApiCall, ) -> None: """Test the GET method with JSON response.""" - with requests_mock.mock() as request_mocker: - request_mocker.get( - "http://nearest:8108/test", - json={"key": "value"}, - status_code=200, + with respx.mock: + respx.get("http://nearest:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) assert fake_api_call.get( "/test", @@ -269,11 +256,9 @@ def test_get_as_text( fake_api_call: ApiCall, ) -> None: """Test the GET method with text response.""" - with requests_mock.mock() as request_mocker: - request_mocker.get( - "http://nearest:8108/test", - text="response text", - status_code=200, + with respx.mock: + respx.get("http://nearest:8108/test").mock( + return_value=httpx.Response(200, text="response text") ) assert ( fake_api_call.get("/test", as_json=False, entity_type=typing.Dict[str, str]) @@ -285,11 +270,9 @@ def test_post_as_json( fake_api_call: ApiCall, ) -> None: """Test the POST method with JSON response.""" - with requests_mock.mock() as request_mocker: - request_mocker.post( - "http://nearest:8108/test", - json={"key": "value"}, - status_code=200, + with respx.mock: + respx.post("http://nearest:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) assert fake_api_call.post( "/test", @@ -305,11 +288,9 @@ def test_post_with_params( fake_api_call: ApiCall, ) -> None: """Test that the parameters are correctly passed to the request.""" - with requests_mock.Mocker() as request_mocker: - request_mocker.post( - "http://nearest:8108/test", - json={"key": "value"}, - status_code=200, + with respx.mock: + route = respx.post("http://nearest:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) parameter_set = {"key1": [True, False], "key2": False, "key3": "value"} @@ -328,9 +309,15 @@ def test_post_with_params( "key3": ["value"], } - request = request_mocker.request_history[0] - - assert request.qs == expected_parameter_set + request = route.calls.last.request + # respx stores params as a MultiDict, convert to dict for comparison + params_dict: typing.Dict[str, typing.List[str]] = {} + for key, value in request.url.params.multi_items(): + if key in params_dict: + params_dict[key].append(value) + else: + params_dict[key] = [value] + assert params_dict == expected_parameter_set assert post_result == {"key": "value"} @@ -338,11 +325,9 @@ def test_post_as_text( fake_api_call: ApiCall, ) -> None: """Test the POST method with text response.""" - with requests_mock.mock() as request_mocker: - request_mocker.post( - "http://nearest:8108/test", - text="response text", - status_code=200, + with respx.mock: + respx.post("http://nearest:8108/test").mock( + return_value=httpx.Response(200, text="response text") ) post_result = fake_api_call.post( "/test", @@ -357,11 +342,9 @@ def test_put_as_json( fake_api_call: ApiCall, ) -> None: """Test the PUT method with JSON response.""" - with requests_mock.mock() as request_mocker: - request_mocker.put( - "http://nearest:8108/test", - json={"key": "value"}, - status_code=200, + with respx.mock: + respx.put("http://nearest:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) assert fake_api_call.put( "/test", @@ -374,11 +357,9 @@ def test_patch_as_json( fake_api_call: ApiCall, ) -> None: """Test the PATCH method with JSON response.""" - with requests_mock.mock() as request_mocker: - request_mocker.patch( - "http://nearest:8108/test", - json={"key": "value"}, - status_code=200, + with respx.mock: + respx.patch("http://nearest:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) assert fake_api_call.patch( "/test", @@ -391,11 +372,9 @@ def test_delete_as_json( fake_api_call: ApiCall, ) -> None: """Test the DELETE method with JSON response.""" - with requests_mock.mock() as request_mocker: - request_mocker.delete( - "http://nearest:8108/test", - json={"key": "value"}, - status_code=200, + with respx.mock: + respx.delete("http://nearest:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) response = fake_api_call.delete("/test", entity_type=typing.Dict[str, str]) @@ -406,63 +385,66 @@ def test_raise_custom_exception_with_header( fake_api_call: ApiCall, ) -> None: """Test that it raises a custom exception with the error message.""" - with requests_mock.mock() as request_mocker: - request_mocker.get( - "http://nearest:8108/test", - json={"message": "Test error"}, - status_code=400, - headers={"Content-Type": "application/json"}, + with respx.mock: + respx.get("http://nearest:8108/test").mock( + return_value=httpx.Response( + 400, + json={"message": "Test error"}, + headers={"Content-Type": "application/json"}, + ) ) with pytest.raises(exceptions.RequestMalformed) as exception: fake_api_call._execute_request( - requests.get, + "GET", "/test", as_json=True, entity_type=typing.Dict[str, str], ) - assert str(exception.value) == "[Errno 400] Test error" + assert str(exception.value) == "[Errno 400] Test error" def test_raise_custom_exception_without_header( fake_api_call: ApiCall, ) -> None: """Test that it raises a custom exception with the error message.""" - with requests_mock.mock() as request_mocker: - request_mocker.get( - "http://nearest:8108/test", - json={"message": "Test error"}, - status_code=400, + with respx.mock: + # Use content instead of json to avoid automatic Content-Type header + # This tests the case where Content-Type is not application/json + respx.get("http://nearest:8108/test").mock( + return_value=httpx.Response( + 400, + content=b'{"message": "Test error"}', + headers={"Content-Type": "text/plain"}, + ) ) with pytest.raises(exceptions.RequestMalformed) as exception: fake_api_call._execute_request( - requests.get, + "GET", "/test", as_json=True, entity_type=typing.Dict[str, str], ) - assert str(exception.value) == "[Errno 400] API error." + assert ( + str(exception.value) == '[Errno 400] API error. {"message": "Test error"}' + ) def test_selects_next_available_node_on_timeout( fake_api_call: ApiCall, ) -> None: """Test that it selects the next available node if the request times out.""" - with requests_mock.mock() as request_mocker: + with respx.mock: fake_api_call.config.nearest_node = None - request_mocker.get( - "http://node0:8108/test", - exc=requests.exceptions.ConnectTimeout, + respx.get("http://node0:8108/test").mock( + side_effect=httpx.ConnectTimeout("Timeout") ) - request_mocker.get( - "http://node1:8108/test", - exc=requests.exceptions.ConnectTimeout, + respx.get("http://node1:8108/test").mock( + side_effect=httpx.ConnectTimeout("Timeout") ) - request_mocker.get( - "http://node2:8108/test", - json={"key": "value"}, - status_code=200, + respx.get("http://node2:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}) ) response = fake_api_call.get( @@ -472,10 +454,10 @@ def test_selects_next_available_node_on_timeout( ) assert response == {"key": "value"} - assert request_mocker.request_history[0].url == "http://node0:8108/test" - assert request_mocker.request_history[1].url == "http://node1:8108/test" - assert request_mocker.request_history[2].url == "http://node2:8108/test" - assert request_mocker.call_count == 3 + assert respx.calls[0].request.url == "http://node0:8108/test" + assert respx.calls[1].request.url == "http://node1:8108/test" + assert respx.calls[2].request.url == "http://node2:8108/test" + assert len(respx.calls) == 3 def test_get_node_no_healthy_nodes( @@ -515,16 +497,21 @@ def test_raises_if_no_nodes_are_healthy_with_the_last_exception( fake_api_call: ApiCall, ) -> None: """Test that it raises the last exception if no nodes are healthy.""" - with requests_mock.mock() as request_mocker: - request_mocker.get( - "http://nearest:8108/", - exc=requests.exceptions.ConnectTimeout, + with respx.mock: + respx.get("http://nearest:8108/").mock( + side_effect=httpx.ConnectTimeout("Timeout") + ) + respx.get("http://node0:8108/").mock( + side_effect=httpx.ConnectTimeout("Timeout") + ) + respx.get("http://node1:8108/").mock( + side_effect=httpx.ConnectTimeout("Timeout") + ) + respx.get("http://node2:8108/").mock( + side_effect=httpx.ConnectError("SSL Error") ) - request_mocker.get("http://node0:8108/", exc=requests.exceptions.ConnectTimeout) - request_mocker.get("http://node1:8108/", exc=requests.exceptions.ConnectTimeout) - request_mocker.get("http://node2:8108/", exc=requests.exceptions.SSLError) - with pytest.raises(requests.exceptions.SSLError): + with pytest.raises(httpx.ConnectError): fake_api_call.get("/", entity_type=typing.Dict[str, str]) @@ -533,17 +520,17 @@ def test_uses_nearest_node_if_present_and_healthy( # noqa: WPS213 fake_api_call: ApiCall, ) -> None: """Test that it uses the nearest node if it is present and healthy.""" - with requests_mock.Mocker() as request_mocker: - request_mocker.get( - "http://nearest:8108/", - exc=requests.exceptions.ConnectTimeout, + with respx.mock: + nearest_route = respx.get("http://nearest:8108/") + nearest_route.mock(side_effect=httpx.ConnectTimeout("Timeout")) + respx.get("http://node0:8108/").mock( + side_effect=httpx.ConnectTimeout("Timeout") + ) + respx.get("http://node1:8108/").mock( + side_effect=httpx.ConnectTimeout("Timeout") ) - request_mocker.get("http://node0:8108/", exc=requests.exceptions.ConnectTimeout) - request_mocker.get("http://node1:8108/", exc=requests.exceptions.ConnectTimeout) - request_mocker.get( - "http://node2:8108/", - json={"message": "Success"}, - status_code=200, + respx.get("http://node2:8108/").mock( + return_value=httpx.Response(200, json={"message": "Success"}) ) # Freeze time @@ -582,10 +569,8 @@ def test_uses_nearest_node_if_present_and_healthy( # noqa: WPS213 mocker.patch("time.time", return_value=current_time + 185) # Resolve the request on the nearest node - request_mocker.get( - "http://nearest:8108/", - json={"message": "Success"}, - status_code=200, + nearest_route.mock( + return_value=httpx.Response(200, json={"message": "Success"}) ) # 1 should go to nearest and resolve the request: 1 request @@ -596,24 +581,24 @@ def test_uses_nearest_node_if_present_and_healthy( # noqa: WPS213 fake_api_call.get("/", entity_type=typing.Dict[str, str]) # Check the request history - assert request_mocker.request_history[0].url == "http://nearest:8108/" - assert request_mocker.request_history[1].url == "http://node0:8108/" - assert request_mocker.request_history[2].url == "http://node1:8108/" - assert request_mocker.request_history[3].url == "http://node2:8108/" + assert str(respx.calls[0].request.url) == "http://nearest:8108/" + assert str(respx.calls[1].request.url) == "http://node0:8108/" + assert str(respx.calls[2].request.url) == "http://node1:8108/" + assert str(respx.calls[3].request.url) == "http://node2:8108/" - assert request_mocker.request_history[4].url == "http://node2:8108/" - assert request_mocker.request_history[5].url == "http://node2:8108/" + assert str(respx.calls[4].request.url) == "http://node2:8108/" + assert str(respx.calls[5].request.url) == "http://node2:8108/" - assert request_mocker.request_history[6].url == "http://node2:8108/" + assert str(respx.calls[6].request.url) == "http://node2:8108/" - assert request_mocker.request_history[7].url == "http://nearest:8108/" - assert request_mocker.request_history[8].url == "http://node0:8108/" - assert request_mocker.request_history[9].url == "http://node1:8108/" - assert request_mocker.request_history[10].url == "http://node2:8108/" + assert str(respx.calls[7].request.url) == "http://nearest:8108/" + assert str(respx.calls[8].request.url) == "http://node0:8108/" + assert str(respx.calls[9].request.url) == "http://node1:8108/" + assert str(respx.calls[10].request.url) == "http://node2:8108/" - assert request_mocker.request_history[11].url == "http://nearest:8108/" - assert request_mocker.request_history[12].url == "http://nearest:8108/" - assert request_mocker.request_history[13].url == "http://nearest:8108/" + assert str(respx.calls[11].request.url) == "http://nearest:8108/" + assert str(respx.calls[12].request.url) == "http://nearest:8108/" + assert str(respx.calls[13].request.url) == "http://nearest:8108/" def test_max_retries_no_last_exception(fake_api_call: ApiCall) -> None: @@ -623,7 +608,7 @@ def test_max_retries_no_last_exception(fake_api_call: ApiCall) -> None: match="All nodes are unhealthy", ): fake_api_call._execute_request( - requests.get, + "GET", "/", as_json=True, entity_type=typing.Dict[str, str], diff --git a/tests/fixtures/api_call_fixtures.py b/tests/fixtures/api_call_fixtures.py index dde0c3b..4ef53d2 100644 --- a/tests/fixtures/api_call_fixtures.py +++ b/tests/fixtures/api_call_fixtures.py @@ -3,6 +3,7 @@ import pytest from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall from typesense.configuration import Configuration @@ -18,3 +19,18 @@ def fake_api_call_fixture( def actual_api_call_fixture(actual_config: Configuration) -> ApiCall: """Return an ApiCall object using a real API.""" return ApiCall(actual_config) + + +@pytest.fixture(scope="function", name="actual_async_api_call") +def actual_async_api_call_fixture(actual_config: Configuration) -> AsyncApiCall: + """Return an AsyncApiCall object using a real API.""" + return AsyncApiCall(actual_config) + + +@pytest.fixture(scope="function", name="fake_async_api_call") +def fake_api_call_async_fixture( + fake_config: Configuration, +) -> AsyncApiCall: + """Return an ApiCall object with test values.""" + return AsyncApiCall(fake_config) + From c2304216b0c77539d1440912f78cde4101766e4c Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:39:44 +0200 Subject: [PATCH 10/32] feat(alias): add async support for alias operations - add asyncalias class for async individual alias operations - add asyncaliases class for async alias collection operations - add async tests for alias and aliases functionality - add async fixtures for testing async alias operations - remove future annotations imports from test files --- src/typesense/async_alias.py | 80 +++++++++++++++++++ src/typesense/async_aliases.py | 129 +++++++++++++++++++++++++++++++ tests/alias_test.py | 57 +++++++++++++- tests/aliases_test.py | 103 +++++++++++++++++++++++- tests/fixtures/alias_fixtures.py | 25 ++++++ 5 files changed, 392 insertions(+), 2 deletions(-) create mode 100644 src/typesense/async_alias.py create mode 100644 src/typesense/async_aliases.py diff --git a/src/typesense/async_alias.py b/src/typesense/async_alias.py new file mode 100644 index 0000000..0e542f1 --- /dev/null +++ b/src/typesense/async_alias.py @@ -0,0 +1,80 @@ +""" +This module provides async functionality for managing individual aliases in Typesense. + +It contains the AsyncAlias class, which allows for retrieving and deleting +aliases asynchronously. + +Classes: + AsyncAlias: Manages async operations on a single alias in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.types.alias: Provides AliasSchema type. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.types.alias import AliasSchema + + +class AsyncAlias: + """ + Manages async operations on a single alias in the Typesense API. + + This class provides async methods to retrieve and delete an alias. + + Attributes: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + name (str): The name of the alias. + """ + + def __init__(self, api_call: AsyncApiCall, name: str): + """ + Initialize the AsyncAlias instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + name (str): The name of the alias. + """ + self.api_call = api_call + self.name = name + + async def retrieve(self) -> AliasSchema: + """ + Retrieve this specific alias. + + Returns: + AliasSchema: The schema containing the alias details. + """ + response: AliasSchema = await self.api_call.get( + self._endpoint_path, + entity_type=AliasSchema, + as_json=True, + ) + return response + + async def delete(self) -> AliasSchema: + """ + Delete this specific alias. + + Returns: + AliasSchema: The schema containing the deletion response. + """ + response: AliasSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=AliasSchema, + ) + return response + + @property + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific alias. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_aliases import AsyncAliases + + return "/".join([AsyncAliases.resource_path, self.name]) diff --git a/src/typesense/async_aliases.py b/src/typesense/async_aliases.py new file mode 100644 index 0000000..e247584 --- /dev/null +++ b/src/typesense/async_aliases.py @@ -0,0 +1,129 @@ +""" +This module provides async functionality for managing aliases in Typesense. + +It contains the AsyncAliases class, which allows for creating, updating, retrieving, and +accessing individual aliases asynchronously. + +Classes: + AsyncAliases: Manages aliases in the Typesense API (async). + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.async_alias: Provides the AsyncAlias class for individual alias operations. + - typesense.types.alias: Provides AliasCreateSchema, AliasSchema, and AliasesResponseSchema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +from typesense.async_api_call import AsyncApiCall +from typesense.async_alias import AsyncAlias +from typesense.types.alias import AliasCreateSchema, AliasSchema, AliasesResponseSchema + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + + +class AsyncAliases: + """ + Manages aliases in the Typesense API (async). + + This class provides async methods to create, update, retrieve, and access individual aliases. + + Attributes: + resource_path (str): The API endpoint path for alias operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + aliases (Dict[str, AsyncAlias]): A dictionary of AsyncAlias instances, keyed by alias name. + """ + + resource_path: typing.Final[str] = "/aliases" + + def __init__(self, api_call: AsyncApiCall): + """ + Initialize the AsyncAliases instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + self.aliases: typing.Dict[str, AsyncAlias] = {} + + def __getitem__(self, name: str) -> AsyncAlias: + """ + Get or create an AsyncAlias instance for a given alias name. + + This method allows accessing aliases using dictionary-like syntax. + If the AsyncAlias instance doesn't exist, it creates a new one. + + Args: + name (str): The name of the alias. + + Returns: + AsyncAlias: The AsyncAlias instance for the specified alias name. + + Example: + >>> aliases = AsyncAliases(async_api_call) + >>> company_alias = aliases["company_alias"] + """ + if not self.aliases.get(name): + self.aliases[name] = AsyncAlias(self.api_call, name) + return self.aliases.get(name) + + async def upsert(self, name: str, mapping: AliasCreateSchema) -> AliasSchema: + """ + Create or update an alias. + + Args: + name (str): The name of the alias. + mapping (AliasCreateSchema): The schema for creating or updating the alias. + + Returns: + AliasSchema: The created or updated alias. + + Example: + >>> aliases = AsyncAliases(async_api_call) + >>> alias = await aliases.upsert( + ... "company_alias", {"collection_name": "companies"} + ... ) + """ + response: AliasSchema = await self.api_call.put( + self._endpoint_path(name), + body=mapping, + entity_type=AliasSchema, + ) + return response + + async def retrieve(self) -> AliasesResponseSchema: + """ + Retrieve all aliases. + + Returns: + AliasesResponseSchema: The schema containing all aliases. + + Example: + >>> aliases = AsyncAliases(async_api_call) + >>> all_aliases = await aliases.retrieve() + >>> for alias in all_aliases["aliases"]: + ... print(alias["name"]) + """ + response: AliasesResponseSchema = await self.api_call.get( + AsyncAliases.resource_path, + as_json=True, + entity_type=AliasesResponseSchema, + ) + return response + + def _endpoint_path(self, alias_name: str) -> str: + """ + Construct the API endpoint path for alias operations. + + Args: + alias_name (str): The name of the alias. + + Returns: + str: The constructed endpoint path. + """ + return "/".join([AsyncAliases.resource_path, alias_name]) diff --git a/tests/alias_test.py b/tests/alias_test.py index a454561..483716c 100644 --- a/tests/alias_test.py +++ b/tests/alias_test.py @@ -1,6 +1,5 @@ """Tests for the Alias class.""" -from __future__ import annotations from tests.utils.object_assertions import ( assert_match_object, assert_object_lists_match, @@ -9,6 +8,9 @@ from typesense.alias import Alias from typesense.aliases import Aliases from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_alias import AsyncAlias +from typesense.async_aliases import AsyncAliases def test_init(fake_api_call: ApiCall) -> None: @@ -28,6 +30,23 @@ def test_init(fake_api_call: ApiCall) -> None: assert alias._endpoint_path == "/aliases/company_alias" # noqa: WPS437 +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncAlias object is initialized correctly.""" + alias = AsyncAlias(fake_async_api_call, "company_alias") + + assert alias.name == "company_alias" + assert_match_object(alias.api_call, fake_async_api_call) + assert_object_lists_match( + alias.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + alias.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert alias._endpoint_path == "/aliases/company_alias" # noqa: WPS437 + + def test_actual_retrieve( actual_aliases: Aliases, delete_all_aliases: None, @@ -62,3 +81,39 @@ def test_actual_delete( "collection_name": "companies", "name": "company_alias", } + + +async def test_actual_retrieve_async( + actual_async_aliases: AsyncAliases, + delete_all_aliases: None, + delete_all: None, + create_alias: None, +) -> None: + """Test that the AsyncAlias object can retrieve an alias from Typesense Server.""" + response = await actual_async_aliases["company_alias"].retrieve() + + assert response["collection_name"] == "companies" + assert response["name"] == "company_alias" + + assert_to_contain_object( + response, + { + "collection_name": "companies", + "name": "company_alias", + }, + ) + + +async def test_actual_delete_async( + actual_async_aliases: AsyncAliases, + delete_all_aliases: None, + delete_all: None, + create_alias: None, +) -> None: + """Test that the AsyncAlias object can delete an alias from Typesense Server.""" + response = await actual_async_aliases["company_alias"].delete() + + assert response == { + "collection_name": "companies", + "name": "company_alias", + } diff --git a/tests/aliases_test.py b/tests/aliases_test.py index b1a1adf..6d7fb4a 100644 --- a/tests/aliases_test.py +++ b/tests/aliases_test.py @@ -1,6 +1,5 @@ """Tests for the Aliases class.""" -from __future__ import annotations from tests.utils.object_assertions import ( assert_match_object, assert_object_lists_match, @@ -8,6 +7,8 @@ ) from typesense.aliases import Aliases from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_aliases import AsyncAliases def test_init(fake_api_call: ApiCall) -> None: @@ -27,6 +28,23 @@ def test_init(fake_api_call: ApiCall) -> None: assert not aliases.aliases +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncAliases object is initialized correctly.""" + aliases = AsyncAliases(fake_async_api_call) + + assert_match_object(aliases.api_call, fake_async_api_call) + assert_object_lists_match( + aliases.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + aliases.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not aliases.aliases + + def test_get_missing_alias(fake_aliases: Aliases) -> None: """Test that the Aliases object can get a missing alias.""" alias = fake_aliases["company_alias"] @@ -43,6 +61,23 @@ def test_get_missing_alias(fake_aliases: Aliases) -> None: assert alias._endpoint_path == "/aliases/company_alias" # noqa: WPS437 +def test_get_missing_alias_async(fake_async_aliases: AsyncAliases) -> None: + """Test that the AsyncAliases object can get a missing alias.""" + alias = fake_async_aliases["company_alias"] + + assert alias.name == "company_alias" + assert_match_object(alias.api_call, fake_async_aliases.api_call) + assert_object_lists_match( + alias.api_call.node_manager.nodes, + fake_async_aliases.api_call.node_manager.nodes, + ) + assert_match_object( + alias.api_call.config.nearest_node, + fake_async_aliases.api_call.config.nearest_node, + ) + assert alias._endpoint_path == "/aliases/company_alias" # noqa: WPS437 + + def test_get_existing_alias(fake_aliases: Aliases) -> None: """Test that the Aliases object can get an existing alias.""" alias = fake_aliases["companies"] @@ -53,6 +88,16 @@ def test_get_existing_alias(fake_aliases: Aliases) -> None: assert alias is fetched_alias +def test_get_existing_alias_async(fake_async_aliases: AsyncAliases) -> None: + """Test that the AsyncAliases object can get an existing alias.""" + alias = fake_async_aliases["companies"] + fetched_alias = fake_async_aliases["companies"] + + assert len(fake_async_aliases.aliases) == 1 + + assert alias is fetched_alias + + def test_actual_create(actual_aliases: Aliases, delete_all_aliases: None) -> None: """Test that the Aliases object can create an alias on Typesense Server.""" response = actual_aliases.upsert("company_alias", {"collection_name": "companies"}) @@ -103,3 +148,59 @@ def test_actual_retrieve( "name": "company_alias", }, ) + + +async def test_actual_create_async( + actual_async_aliases: AsyncAliases, delete_all_aliases: None +) -> None: + """Test that the AsyncAliases object can create an alias on Typesense Server.""" + response = await actual_async_aliases.upsert( + "company_alias", {"collection_name": "companies"} + ) + + assert response == {"collection_name": "companies", "name": "company_alias"} + + +async def test_actual_update_async( + actual_async_aliases: AsyncAliases, + delete_all_aliases: None, + delete_all: None, + create_collection: None, + create_another_collection: None, +) -> None: + """Test that the AsyncAliases object can update an alias on Typesense Server.""" + create_response = await actual_async_aliases.upsert( + "company_alias", + {"collection_name": "companies"}, + ) + + assert create_response == {"collection_name": "companies", "name": "company_alias"} + + update_response = await actual_async_aliases.upsert( + "company_alias", + {"collection_name": "companies_2"}, + ) + + assert update_response == { + "collection_name": "companies_2", + "name": "company_alias", + } + + +async def test_actual_retrieve_async( + delete_all: None, + delete_all_aliases: None, + create_alias: None, + actual_async_aliases: AsyncAliases, +) -> None: + """Test that the AsyncAliases object can retrieve an alias from Typesense Server.""" + response = await actual_async_aliases.retrieve() + + assert len(response["aliases"]) == 1 + assert_to_contain_object( + response["aliases"][0], + { + "collection_name": "companies", + "name": "company_alias", + }, + ) diff --git a/tests/fixtures/alias_fixtures.py b/tests/fixtures/alias_fixtures.py index b226301..dfc0d69 100644 --- a/tests/fixtures/alias_fixtures.py +++ b/tests/fixtures/alias_fixtures.py @@ -6,6 +6,9 @@ from typesense.alias import Alias from typesense.aliases import Aliases from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_alias import AsyncAlias +from typesense.async_aliases import AsyncAliases @pytest.fixture(scope="function", name="delete_all_aliases") @@ -62,3 +65,25 @@ def fake_aliases_fixture(fake_api_call: ApiCall) -> Aliases: def fake_alias_fixture(fake_api_call: ApiCall) -> Alias: """Return a Alias object with test values.""" return Alias(fake_api_call, "company_alias") + + +@pytest.fixture(scope="function", name="actual_async_aliases") +def actual_async_aliases_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncAliases: + """Return a AsyncAliases object using a real API.""" + return AsyncAliases(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_aliases") +def fake_async_aliases_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncAliases: + """Return a AsyncAliases object with test values.""" + return AsyncAliases(fake_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_alias") +def fake_async_alias_fixture(fake_async_api_call: AsyncApiCall) -> AsyncAlias: + """Return a AsyncAlias object with test values.""" + return AsyncAlias(fake_async_api_call, "company_alias") From dd2abf241206225ea3e49522a4166385a2936419 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:41:24 +0200 Subject: [PATCH 11/32] feat(analytics): add async support for analytics operations - add async tests for analytics functionality - remove future annotations imports from test files --- src/typesense/async_analytics.py | 14 +++++ src/typesense/async_analytics_events.py | 71 +++++++++++++++++++++ src/typesense/async_analytics_rule.py | 31 +++++++++ src/typesense/async_analytics_rules.py | 62 ++++++++++++++++++ tests/analytics_events_test.py | 83 ++++++++++++++++++++++++- tests/analytics_rule_test.py | 23 ++++++- tests/analytics_rules_test.py | 67 ++++++++++++++++++++ tests/analytics_test.py | 19 ++++++ tests/fixtures/analytics_fixtures.py | 28 +++++++++ 9 files changed, 394 insertions(+), 4 deletions(-) create mode 100644 src/typesense/async_analytics.py create mode 100644 src/typesense/async_analytics_events.py create mode 100644 src/typesense/async_analytics_rule.py create mode 100644 src/typesense/async_analytics_rules.py diff --git a/src/typesense/async_analytics.py b/src/typesense/async_analytics.py new file mode 100644 index 0000000..a202d17 --- /dev/null +++ b/src/typesense/async_analytics.py @@ -0,0 +1,14 @@ +"""Client for Typesense Analytics module (async).""" + +from typesense.async_analytics_events import AsyncAnalyticsEvents +from typesense.async_analytics_rules import AsyncAnalyticsRules +from typesense.async_api_call import AsyncApiCall + + +class AsyncAnalytics: + """Client for v30 Analytics endpoints (async).""" + + def __init__(self, api_call: AsyncApiCall) -> None: + self.api_call = api_call + self.rules = AsyncAnalyticsRules(api_call) + self.events = AsyncAnalyticsEvents(api_call) diff --git a/src/typesense/async_analytics_events.py b/src/typesense/async_analytics_events.py new file mode 100644 index 0000000..fa7a763 --- /dev/null +++ b/src/typesense/async_analytics_events.py @@ -0,0 +1,71 @@ +"""Client for Analytics events and status operations (async).""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.types.analytics import ( + AnalyticsEvent as AnalyticsEventSchema, + AnalyticsEventCreateResponse, + AnalyticsEventsResponse, + AnalyticsStatus, +) + + +class AsyncAnalyticsEvents: + events_path: typing.Final[str] = "/analytics/events" + flush_path: typing.Final[str] = "/analytics/flush" + status_path: typing.Final[str] = "/analytics/status" + + def __init__(self, api_call: AsyncApiCall) -> None: + self.api_call = api_call + + async def create(self, event: AnalyticsEventSchema) -> AnalyticsEventCreateResponse: + response: AnalyticsEventCreateResponse = await self.api_call.post( + AsyncAnalyticsEvents.events_path, + body=event, + as_json=True, + entity_type=AnalyticsEventCreateResponse, + ) + return response + + async def retrieve( + self, + *, + user_id: str, + name: str, + n: int, + ) -> AnalyticsEventsResponse: + params: typing.Dict[str, typing.Union[str, int]] = { + "user_id": user_id, + "name": name, + "n": n, + } + response: AnalyticsEventsResponse = await self.api_call.get( + AsyncAnalyticsEvents.events_path, + params=params, + as_json=True, + entity_type=AnalyticsEventsResponse, + ) + return response + + async def flush(self) -> AnalyticsEventCreateResponse: + response: AnalyticsEventCreateResponse = await self.api_call.post( + AsyncAnalyticsEvents.flush_path, + body={}, + as_json=True, + entity_type=AnalyticsEventCreateResponse, + ) + return response + + async def status(self) -> AnalyticsStatus: + response: AnalyticsStatus = await self.api_call.get( + AsyncAnalyticsEvents.status_path, + as_json=True, + entity_type=AnalyticsStatus, + ) + return response diff --git a/src/typesense/async_analytics_rule.py b/src/typesense/async_analytics_rule.py new file mode 100644 index 0000000..8d378a4 --- /dev/null +++ b/src/typesense/async_analytics_rule.py @@ -0,0 +1,31 @@ +"""Per-rule client for Analytics rules operations (async).""" + +from typesense.async_api_call import AsyncApiCall +from typesense.types.analytics import AnalyticsRuleSchema + + +class AsyncAnalyticsRule: + def __init__(self, api_call: AsyncApiCall, rule_name: str) -> None: + self.api_call = api_call + self.rule_name = rule_name + + @property + def _endpoint_path(self) -> str: + from typesense.async_analytics_rules import AsyncAnalyticsRules + + return "/".join([AsyncAnalyticsRules.resource_path, self.rule_name]) + + async def retrieve(self) -> AnalyticsRuleSchema: + response: AnalyticsRuleSchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=AnalyticsRuleSchema, + ) + return response + + async def delete(self) -> AnalyticsRuleSchema: + response: AnalyticsRuleSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=AnalyticsRuleSchema, + ) + return response diff --git a/src/typesense/async_analytics_rules.py b/src/typesense/async_analytics_rules.py new file mode 100644 index 0000000..da58cb9 --- /dev/null +++ b/src/typesense/async_analytics_rules.py @@ -0,0 +1,62 @@ +"""Client for Analytics rules collection operations (async).""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_analytics_rule import AsyncAnalyticsRule +from typesense.async_api_call import AsyncApiCall +from typesense.types.analytics import ( + AnalyticsRuleCreate, + AnalyticsRuleSchema, + AnalyticsRuleUpdate, +) + + +class AsyncAnalyticsRules(object): + resource_path: typing.Final[str] = "/analytics/rules" + + def __init__(self, api_call: AsyncApiCall) -> None: + self.api_call = api_call + self.rules: typing.Dict[str, AsyncAnalyticsRule] = {} + + def __getitem__(self, rule_name: str) -> AsyncAnalyticsRule: + if rule_name not in self.rules: + self.rules[rule_name] = AsyncAnalyticsRule(self.api_call, rule_name) + return self.rules[rule_name] + + async def create(self, rule: AnalyticsRuleCreate) -> AnalyticsRuleSchema: + response: AnalyticsRuleSchema = await self.api_call.post( + AsyncAnalyticsRules.resource_path, + body=rule, + as_json=True, + entity_type=AnalyticsRuleSchema, + ) + return response + + async def retrieve( + self, *, rule_tag: typing.Union[str, None] = None + ) -> typing.List[AnalyticsRuleSchema]: + params: typing.Dict[str, str] = {} + if rule_tag: + params["rule_tag"] = rule_tag + response: typing.List[AnalyticsRuleSchema] = await self.api_call.get( + AsyncAnalyticsRules.resource_path, + params=params if params else None, + as_json=True, + entity_type=typing.List[AnalyticsRuleSchema], + ) + return response + + async def upsert( + self, rule_name: str, update: AnalyticsRuleUpdate + ) -> AnalyticsRuleSchema: + response: AnalyticsRuleSchema = await self.api_call.put( + "/".join([AsyncAnalyticsRules.resource_path, rule_name]), + body=update, + entity_type=AnalyticsRuleSchema, + ) + return response diff --git a/tests/analytics_events_test.py b/tests/analytics_events_test.py index 965df58..a423d6c 100644 --- a/tests/analytics_events_test.py +++ b/tests/analytics_events_test.py @@ -1,10 +1,10 @@ """Tests for Analytics events endpoints (client.analytics.events).""" -from __future__ import annotations - import pytest from tests.utils.version import is_v30_or_above +from typesense.async_analytics_events import AsyncAnalyticsEvents +from typesense.async_analytics_rules import AsyncAnalyticsRules from typesense.client import Client from typesense.types.analytics import AnalyticsEvent @@ -127,3 +127,82 @@ def test_acutal_retrieve_events( def test_acutal_flush(actual_client: Client, delete_all: None) -> None: resp = actual_client.analytics.events.flush() assert resp["ok"] in [True, False] + + +async def test_actual_create_event_async( + actual_async_analytics_rules: AsyncAnalyticsRules, + actual_async_analytics_events: AsyncAnalyticsEvents, + delete_all: None, + create_collection: None, + delete_all_analytics_rules: None, +) -> None: + await actual_async_analytics_rules.create( + { + "name": "company_analytics_rule", + "type": "log", + "collection": "companies", + "event_type": "click", + "params": {}, + } + ) + event: AnalyticsEvent = { + "name": "company_analytics_rule", + "event_type": "query", + "data": { + "user_id": "user-1", + "doc_id": "apple", + }, + } + resp = await actual_async_analytics_events.create(event) + assert resp["ok"] is True + await actual_async_analytics_rules["company_analytics_rule"].delete() + + +async def test_status_async( + actual_async_analytics_events: AsyncAnalyticsEvents, + delete_all: None, +) -> None: + status = await actual_async_analytics_events.status() + assert isinstance(status, dict) + + +async def test_retrieve_events_async( + actual_async_analytics_rules: AsyncAnalyticsRules, + actual_async_analytics_events: AsyncAnalyticsEvents, + delete_all: None, + create_collection: None, + delete_all_analytics_rules: None, +) -> None: + await actual_async_analytics_rules.create( + { + "name": "company_analytics_rule", + "type": "log", + "collection": "companies", + "event_type": "click", + "params": {}, + } + ) + event: AnalyticsEvent = { + "name": "company_analytics_rule", + "event_type": "query", + "data": { + "user_id": "user-1", + "doc_id": "apple", + }, + } + resp = await actual_async_analytics_events.create(event) + assert resp["ok"] is True + result = await actual_async_analytics_events.retrieve( + user_id="user-1", + name="company_analytics_rule", + n=10, + ) + assert "events" in result + + +async def test_actual_flush_async( + actual_async_analytics_events: AsyncAnalyticsEvents, + delete_all: None, +) -> None: + resp = await actual_async_analytics_events.flush() + assert resp["ok"] in [True, False] diff --git a/tests/analytics_rule_test.py b/tests/analytics_rule_test.py index 525aa27..16169c1 100644 --- a/tests/analytics_rule_test.py +++ b/tests/analytics_rule_test.py @@ -1,13 +1,12 @@ """Unit tests for per-rule AnalyticsRule operations.""" -from __future__ import annotations - import pytest from tests.utils.version import is_v30_or_above from typesense.client import Client from typesense.analytics_rule import AnalyticsRule from typesense.analytics_rules import AnalyticsRules +from typesense.async_analytics_rules import AsyncAnalyticsRules pytestmark = pytest.mark.skipif( @@ -41,3 +40,23 @@ def test_actual_rule_delete( ) -> None: resp = actual_analytics_rules["company_analytics_rule"].delete() assert resp["name"] == "company_analytics_rule" + + +async def test_actual_rule_retrieve_async( + actual_async_analytics_rules: AsyncAnalyticsRules, + delete_all: None, + delete_all_analytics_rules: None, + create_analytics_rule: None, +) -> None: + resp = await actual_async_analytics_rules["company_analytics_rule"].retrieve() + assert resp["name"] == "company_analytics_rule" + + +async def test_actual_rule_delete_async( + actual_async_analytics_rules: AsyncAnalyticsRules, + delete_all: None, + delete_all_analytics_rules: None, + create_analytics_rule: None, +) -> None: + resp = await actual_async_analytics_rules["company_analytics_rule"].delete() + assert resp["name"] == "company_analytics_rule" diff --git a/tests/analytics_rules_test.py b/tests/analytics_rules_test.py index fb3b35a..f9abe0f 100644 --- a/tests/analytics_rules_test.py +++ b/tests/analytics_rules_test.py @@ -8,6 +8,8 @@ from typesense.client import Client from typesense.analytics_rules import AnalyticsRules from typesense.analytics_rule import AnalyticsRule +from typesense.async_api_call import AsyncApiCall +from typesense.async_analytics_rules import AsyncAnalyticsRules from typesense.types.analytics import AnalyticsRuleCreate @@ -82,3 +84,68 @@ def test_actual_retrieve( rules = actual_analytics_rules.retrieve() assert isinstance(rules, list) assert any(r.get("name") == "company_analytics_rule" for r in rules) + + +def test_rules_init_async(fake_async_api_call: AsyncApiCall) -> None: + from typesense.async_analytics_rules import AsyncAnalyticsRules + + rules = AsyncAnalyticsRules(fake_async_api_call) + assert rules.rules == {} + + +def test_rule_getitem_async(fake_async_api_call: AsyncApiCall) -> None: + from typesense.async_analytics_rules import AsyncAnalyticsRules + from typesense.async_analytics_rule import AsyncAnalyticsRule + + rules = AsyncAnalyticsRules(fake_async_api_call) + rule = rules["company_analytics_rule"] + assert isinstance(rule, AsyncAnalyticsRule) + assert rule._endpoint_path == "/analytics/rules/company_analytics_rule" + + +async def test_actual_create_async( + actual_async_analytics_rules: AsyncAnalyticsRules, + delete_all: None, + delete_all_analytics_rules: None, + create_collection: None, + create_query_collection: None, +) -> None: + body: AnalyticsRuleCreate = { + "name": "company_analytics_rule", + "type": "nohits_queries", + "collection": "companies", + "event_type": "search", + "params": {"destination_collection": "companies_queries", "limit": 1000}, + } + resp = await actual_async_analytics_rules.create(rule=body) + assert resp["name"] == "company_analytics_rule" + assert resp["params"]["destination_collection"] == "companies_queries" + + +async def test_actual_update_async( + actual_async_analytics_rules: AsyncAnalyticsRules, + delete_all: None, + delete_all_analytics_rules: None, + create_analytics_rule: None, +) -> None: + resp = await actual_async_analytics_rules.upsert( + "company_analytics_rule", + { + "params": { + "destination_collection": "companies_queries", + "limit": 500, + }, + }, + ) + assert resp["name"] == "company_analytics_rule" + + +async def test_actual_retrieve_async( + actual_async_analytics_rules: AsyncAnalyticsRules, + delete_all: None, + delete_all_analytics_rules: None, + create_analytics_rule: None, +) -> None: + rules = await actual_async_analytics_rules.retrieve() + assert isinstance(rules, list) + assert any(r.get("name") == "company_analytics_rule" for r in rules) diff --git a/tests/analytics_test.py b/tests/analytics_test.py index 2ff12b6..bcf0689 100644 --- a/tests/analytics_test.py +++ b/tests/analytics_test.py @@ -6,6 +6,8 @@ from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.analytics import Analytics from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_analytics import AsyncAnalytics @pytest.mark.skipif( @@ -34,3 +36,20 @@ def test_init(fake_api_call: ApiCall) -> None: ) assert not analytics.rules.rules + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncAnalytics object is initialized correctly.""" + analytics = AsyncAnalytics(fake_async_api_call) + + assert_match_object(analytics.rules.api_call, fake_async_api_call) + assert_object_lists_match( + analytics.rules.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + analytics.rules.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not analytics.rules.rules diff --git a/tests/fixtures/analytics_fixtures.py b/tests/fixtures/analytics_fixtures.py index 9097294..d02013d 100644 --- a/tests/fixtures/analytics_fixtures.py +++ b/tests/fixtures/analytics_fixtures.py @@ -6,6 +6,10 @@ from typesense.analytics_rule import AnalyticsRule from typesense.analytics_rules import AnalyticsRules from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_analytics_events import AsyncAnalyticsEvents +from typesense.async_analytics_rule import AsyncAnalyticsRule +from typesense.async_analytics_rules import AsyncAnalyticsRules @pytest.fixture(scope="function", name="delete_all_analytics_rules") @@ -93,3 +97,27 @@ def create_query_collection_fixture() -> None: timeout=3, ) response.raise_for_status() + + +@pytest.fixture(scope="function", name="fake_async_analytics_rules") +def fake_async_analytics_rules_fixture(fake_async_api_call: AsyncApiCall) -> AsyncAnalyticsRules: + """Return an AsyncAnalyticsRules object with test values.""" + return AsyncAnalyticsRules(fake_async_api_call) + + +@pytest.fixture(scope="function", name="actual_async_analytics_rules") +def actual_async_analytics_rules_fixture(actual_async_api_call: AsyncApiCall) -> AsyncAnalyticsRules: + """Return an AsyncAnalyticsRules object using a real API.""" + return AsyncAnalyticsRules(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_analytics_rule") +def fake_async_analytics_rule_fixture(fake_async_api_call: AsyncApiCall) -> AsyncAnalyticsRule: + """Return an AsyncAnalyticsRule object with test values.""" + return AsyncAnalyticsRule(fake_async_api_call, "company_analytics_rule") + + +@pytest.fixture(scope="function", name="actual_async_analytics_events") +def actual_async_analytics_events_fixture(actual_async_api_call: AsyncApiCall) -> AsyncAnalyticsEvents: + """Return an AsyncAnalyticsEvents object using a real API.""" + return AsyncAnalyticsEvents(actual_async_api_call) From 82de1663051d51d9210bb729b9227544166ebba8 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:42:23 +0200 Subject: [PATCH 12/32] feat(analyticsV1): add async support for analytics v1 operations - add async tests for analytics v1 functionality - add async fixtures for testing async analytics v1 operations - remove future annotations imports from test files --- src/typesense/async_analytics_rule_v1.py | 114 ++++++++++++ src/typesense/async_analytics_rules_v1.py | 177 +++++++++++++++++++ src/typesense/async_analytics_v1.py | 49 +++++ tests/analytics_rule_v1_test.py | 34 ++++ tests/analytics_rules_v1_test.py | 134 +++++++++++++- tests/analytics_v1_test.py | 19 ++ tests/fixtures/analytics_rule_v1_fixtures.py | 21 +++ 7 files changed, 546 insertions(+), 2 deletions(-) create mode 100644 src/typesense/async_analytics_rule_v1.py create mode 100644 src/typesense/async_analytics_rules_v1.py create mode 100644 src/typesense/async_analytics_v1.py diff --git a/src/typesense/async_analytics_rule_v1.py b/src/typesense/async_analytics_rule_v1.py new file mode 100644 index 0000000..1c7ed4c --- /dev/null +++ b/src/typesense/async_analytics_rule_v1.py @@ -0,0 +1,114 @@ +""" +This module provides async functionality for managing individual analytics rules in Typesense (V1). + +Classes: + - AsyncAnalyticsRuleV1: Handles async operations related to a specific analytics rule. + +Methods: + - __init__: Initializes the AsyncAnalyticsRuleV1 object. + - _endpoint_path: Constructs the API endpoint path for this specific analytics rule. + - retrieve: Retrieves the details of this specific analytics rule. + - delete: Deletes this specific analytics rule. + +The AsyncAnalyticsRuleV1 class interacts with the Typesense API to manage operations on a +specific analytics rule. It provides methods to retrieve and delete individual rules. + +For more information on analytics, refer to the Analytics & Query Suggestion +[documentation](https://typesense.org/docs/27.0/api/analytics-query-suggestions.html) + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typing_extensions import deprecated + +from typesense.async_api_call import AsyncApiCall +from typesense.logger import warn_deprecation +from typesense.types.analytics_rule_v1 import ( + RuleDeleteSchema, + RuleSchemaForCounters, + RuleSchemaForQueries, +) + + +@deprecated( + "AsyncAnalyticsRuleV1 is deprecated on v30+. Use client.analytics.rules[rule_id] instead." +) +class AsyncAnalyticsRuleV1: + """ + Class for managing individual analytics rules in Typesense (V1) (async). + + This class provides methods to interact with a specific analytics rule, + including retrieving and deleting it. + + Attributes: + api_call (AsyncApiCall): The API call object for making requests. + rule_id (str): The ID of the analytics rule. + """ + + def __init__(self, api_call: AsyncApiCall, rule_id: str): + """ + Initialize the AsyncAnalyticsRuleV1 object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + rule_id (str): The ID of the analytics rule. + """ + self.api_call = api_call + self.rule_id = rule_id + + async def retrieve( + self, + ) -> typing.Union[RuleSchemaForQueries, RuleSchemaForCounters]: + """ + Retrieve this specific analytics rule. + + Returns: + Union[RuleSchemaForQueries, RuleSchemaForCounters]: + The schema containing the rule details. + """ + response: typing.Union[ + RuleSchemaForQueries, RuleSchemaForCounters + ] = await self.api_call.get( + self._endpoint_path, + entity_type=typing.Union[RuleSchemaForQueries, RuleSchemaForCounters], + as_json=True, + ) + return response + + async def delete(self) -> RuleDeleteSchema: + """ + Delete this specific analytics rule. + + Returns: + RuleDeleteSchema: The schema containing the deletion response. + """ + response: RuleDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=RuleDeleteSchema, + ) + + return response + + @property + @warn_deprecation( # type: ignore[untyped-decorator] + "AsyncAnalyticsRuleV1 is deprecated on v30+. Use client.analytics.rules[rule_id] instead.", + flag_name="analytics_rules_v1_deprecation", + ) + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific analytics rule. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_analytics_rules_v1 import AsyncAnalyticsRulesV1 + + return "/".join([AsyncAnalyticsRulesV1.resource_path, self.rule_id]) diff --git a/src/typesense/async_analytics_rules_v1.py b/src/typesense/async_analytics_rules_v1.py new file mode 100644 index 0000000..ff8a440 --- /dev/null +++ b/src/typesense/async_analytics_rules_v1.py @@ -0,0 +1,177 @@ +""" +This module provides async functionality for managing analytics rules in Typesense (V1). + +Classes: + - AsyncAnalyticsRulesV1: Handles async operations related to analytics rules. + +Methods: + - __init__: Initializes the AsyncAnalyticsRulesV1 object. + - __getitem__: Retrieves or creates an AsyncAnalyticsRuleV1 object for a given rule_id. + - create: Creates a new analytics rule. + - upsert: Creates or updates an analytics rule. + - retrieve: Retrieves all analytics rules. + +Attributes: + - resource_path: The API resource path for analytics rules. + +The AsyncAnalyticsRulesV1 class interacts with the Typesense API to manage analytics rule operations. +It provides methods to create, update, and retrieve analytics rules, as well as access +individual AsyncAnalyticsRuleV1 objects. + +For more information on analytics, refer to the Analytics & Query Suggestion +[documentation](https://typesense.org/docs/27.0/api/analytics-query-suggestions.html) + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +import sys + +from typesense.logger import warn_deprecation + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_analytics_rule_v1 import AsyncAnalyticsRuleV1 +from typesense.async_api_call import AsyncApiCall +from typesense.types.analytics_rule_v1 import ( + RuleCreateSchemaForCounters, + RuleCreateSchemaForQueries, + RuleSchemaForCounters, + RuleSchemaForQueries, + RulesRetrieveSchema, +) + +_RuleParams = typing.Union[ + typing.Dict[str, typing.Union[str, int, bool]], + None, +] + + +class AsyncAnalyticsRulesV1(object): + """ + Class for managing analytics rules in Typesense (V1) (async). + + This class provides methods to interact with analytics rules, including + creating, updating, and retrieving them. + + Attributes: + resource_path (str): The API resource path for analytics rules. + api_call (AsyncApiCall): The API call object for making requests. + rules (Dict[str, AsyncAnalyticsRuleV1]): A dictionary of AsyncAnalyticsRuleV1 objects. + """ + + resource_path: typing.Final[str] = "/analytics/rules" + + def __init__(self, api_call: AsyncApiCall): + """ + Initialize the AsyncAnalyticsRulesV1 object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + """ + self.api_call = api_call + self.rules: typing.Dict[str, AsyncAnalyticsRuleV1] = {} + + def __getitem__(self, rule_id: str) -> AsyncAnalyticsRuleV1: + """ + Get or create an AsyncAnalyticsRuleV1 object for a given rule_id. + + Args: + rule_id (str): The ID of the analytics rule. + + Returns: + AsyncAnalyticsRuleV1: The AsyncAnalyticsRuleV1 object for the given ID. + """ + if not self.rules.get(rule_id): + self.rules[rule_id] = AsyncAnalyticsRuleV1(self.api_call, rule_id) + return self.rules[rule_id] + + @warn_deprecation( # type: ignore[untyped-decorator] + "AsyncAnalyticsRulesV1 is deprecated on v30+. Use client.analytics instead.", + flag_name="analytics_rules_v1_deprecation", + ) + async def create( + self, + rule: typing.Union[RuleCreateSchemaForCounters, RuleCreateSchemaForQueries], + rule_parameters: _RuleParams = None, + ) -> typing.Union[RuleSchemaForCounters, RuleSchemaForQueries]: + """ + Create a new analytics rule. + + This method can create both counter rules and query rules. + + Args: + rule (Union[RuleCreateSchemaForCounters, RuleCreateSchemaForQueries]): + The rule schema. Use RuleCreateSchemaForCounters for counter rules + and RuleCreateSchemaForQueries for query rules. + + rule_parameters (_RuleParams, optional): Additional rule parameters. + + Returns: + Union[RuleSchemaForCounters, RuleSchemaForQueries]: + The created rule. Returns RuleSchemaForCounters for counter rules + and RuleSchemaForQueries for query rules. + """ + response: typing.Union[ + RuleSchemaForCounters, RuleSchemaForQueries + ] = await self.api_call.post( + AsyncAnalyticsRulesV1.resource_path, + body=rule, + params=rule_parameters, + as_json=True, + entity_type=typing.Union[ + RuleSchemaForCounters, + RuleSchemaForQueries, + ], + ) + return response + + @warn_deprecation( # type: ignore[untyped-decorator] + "AsyncAnalyticsRulesV1 is deprecated on v30+. Use client.analytics instead.", + flag_name="analytics_rules_v1_deprecation", + ) + async def upsert( + self, + rule_id: str, + rule: typing.Union[RuleCreateSchemaForQueries, RuleSchemaForCounters], + ) -> typing.Union[RuleSchemaForCounters, RuleCreateSchemaForQueries]: + """ + Create or update an analytics rule. + + Args: + rule_id (str): The ID of the rule to upsert. + rule (Union[RuleCreateSchemaForQueries, RuleSchemaForCounters]): The rule schema. + + Returns: + Union[RuleSchemaForCounters, RuleCreateSchemaForQueries]: The upserted rule. + """ + response = await self.api_call.put( + "/".join([self.resource_path, rule_id]), + body=rule, + entity_type=typing.Union[RuleSchemaForQueries, RuleSchemaForCounters], + ) + return typing.cast( + typing.Union[RuleSchemaForCounters, RuleCreateSchemaForQueries], + response, + ) + + @warn_deprecation( # type: ignore[untyped-decorator] + "AsyncAnalyticsRulesV1 is deprecated on v30+. Use client.analytics instead.", + flag_name="analytics_rules_v1_deprecation", + ) + async def retrieve(self) -> RulesRetrieveSchema: + """ + Retrieve all analytics rules. + + Returns: + RulesRetrieveSchema: The schema containing all analytics rules. + """ + response: RulesRetrieveSchema = await self.api_call.get( + AsyncAnalyticsRulesV1.resource_path, + as_json=True, + entity_type=RulesRetrieveSchema, + ) + return response diff --git a/src/typesense/async_analytics_v1.py b/src/typesense/async_analytics_v1.py new file mode 100644 index 0000000..32a2918 --- /dev/null +++ b/src/typesense/async_analytics_v1.py @@ -0,0 +1,49 @@ +""" +This module provides async functionality for managing analytics (V1) in Typesense. + +Classes: + - AsyncAnalyticsV1: Handles async operations related to analytics, including access to analytics rules. + +Methods: + - __init__: Initializes the AsyncAnalyticsV1 object. + +The AsyncAnalyticsV1 class serves as an entry point for analytics-related operations in Typesense, +currently providing access to AsyncAnalyticsRulesV1. + +For more information on analytics, refer to the Analytics & Query Suggestion +[documentation](https://typesense.org/docs/27.0/api/analytics-query-suggestions.html) + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +from typing_extensions import deprecated + +from typesense.async_analytics_rules_v1 import AsyncAnalyticsRulesV1 +from typesense.async_api_call import AsyncApiCall + + +@deprecated("AsyncAnalyticsV1 is deprecated on v30+. Use client.analytics instead.") +class AsyncAnalyticsV1(object): + """ + Class for managing analytics in Typesense (V1) (async). + + This class provides access to analytics-related functionalities, + currently including operations on analytics rules. + + Attributes: + rules (AsyncAnalyticsRulesV1): An instance of AsyncAnalyticsRulesV1 for managing analytics rules. + """ + + def __init__(self, api_call: AsyncApiCall) -> None: + """ + Initialize the AsyncAnalyticsV1 object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + """ + self._rules = AsyncAnalyticsRulesV1(api_call) + + @property + def rules(self) -> AsyncAnalyticsRulesV1: + return self._rules diff --git a/tests/analytics_rule_v1_test.py b/tests/analytics_rule_v1_test.py index 071d3e8..9dcf47b 100644 --- a/tests/analytics_rule_v1_test.py +++ b/tests/analytics_rule_v1_test.py @@ -10,6 +10,7 @@ from typesense.analytics_rule_v1 import AnalyticsRuleV1 from typesense.analytics_rules_v1 import AnalyticsRulesV1 from typesense.api_call import ApiCall +from typesense.async_analytics_rules_v1 import AsyncAnalyticsRulesV1 from typesense.types.analytics_rule_v1 import RuleDeleteSchema, RuleSchemaForQueries pytestmark = pytest.mark.skipif( @@ -45,24 +46,53 @@ def test_init(fake_api_call: ApiCall) -> None: ) +def test_actual_retrieve( + actual_analytics_rules: AnalyticsRulesV1, + delete_all: None, + delete_all_analytics_rules_v1: None, + create_analytics_rule_v1: None, +) -> None: + """Test that the AnalyticsRuleV1 object can retrieve a rule from Typesense Server.""" + response = actual_analytics_rules["company_analytics_rule"].retrieve() + + expected: RuleSchemaForQueries = { "name": "company_analytics_rule", "params": { + "destination": {"collection": "companies_queries"}, + "limit": 1000, "source": {"collections": ["companies"]}, }, "type": "nohits_queries", } + assert response == expected +def test_actual_delete( + actual_analytics_rules: AnalyticsRulesV1, + delete_all: None, + delete_all_analytics_rules_v1: None, + create_analytics_rule_v1: None, +) -> None: + """Test that the AnalyticsRuleV1 object can delete a rule from Typesense Server.""" + response = actual_analytics_rules["company_analytics_rule"].delete() + expected: RuleDeleteSchema = { "name": "company_analytics_rule", } + assert response == expected +async def test_actual_retrieve_async( + actual_async_analytics_rules_v1: AsyncAnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_analytics_rule_v1: None, ) -> None: + """Test that the AsyncAnalyticsRuleV1 object can retrieve a rule from Typesense Server.""" + response = await actual_async_analytics_rules_v1[ + "company_analytics_rule" + ].retrieve() expected: RuleSchemaForQueries = { "name": "company_analytics_rule", @@ -77,10 +107,14 @@ def test_init(fake_api_call: ApiCall) -> None: assert response == expected +async def test_actual_delete_async( + actual_async_analytics_rules_v1: AsyncAnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_analytics_rule_v1: None, ) -> None: + """Test that the AsyncAnalyticsRuleV1 object can delete a rule from Typesense Server.""" + response = await actual_async_analytics_rules_v1["company_analytics_rule"].delete() expected: RuleDeleteSchema = { "name": "company_analytics_rule", diff --git a/tests/analytics_rules_v1_test.py b/tests/analytics_rules_v1_test.py index 76263f9..8675c24 100644 --- a/tests/analytics_rules_v1_test.py +++ b/tests/analytics_rules_v1_test.py @@ -1,7 +1,5 @@ """Tests for the AnalyticsRulesV1 class.""" -from __future__ import annotations - import pytest from tests.utils.object_assertions import assert_match_object, assert_object_lists_match @@ -9,6 +7,8 @@ from typesense.client import Client from typesense.analytics_rules_v1 import AnalyticsRulesV1 from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_analytics_rules_v1 import AsyncAnalyticsRulesV1 from typesense.types.analytics_rule_v1 import ( RuleCreateSchemaForQueries, RulesRetrieveSchema, @@ -75,30 +75,152 @@ def test_get_existing_analytics_rule(fake_analytics_rules: AnalyticsRulesV1) -> assert analytics_rule is fetched_analytics_rule +def test_actual_create( + actual_analytics_rules: AnalyticsRulesV1, + delete_all: None, + delete_all_analytics_rules_v1: None, + create_collection: None, + create_query_collection: None, +) -> None: + """Test that the AnalyticsRulesV1 object can create an analytics_rule on Typesense Server.""" + response = actual_analytics_rules.create( + rule={ + "name": "company_analytics_rule", + "type": "nohits_queries", + "params": { + "source": { + "collections": ["companies"], }, + "destination": {"collection": "companies_queries"}, }, + }, + ) + assert response == { "name": "company_analytics_rule", + "type": "nohits_queries", "params": { "source": {"collections": ["companies"]}, + "destination": {"collection": "companies_queries"}, }, } +def test_actual_update( + actual_analytics_rules: AnalyticsRulesV1, + delete_all: None, + delete_all_analytics_rules_v1: None, + create_analytics_rule_v1: None, +) -> None: + """Test that the AnalyticsRulesV1 object can update an analytics_rule on Typesense Server.""" + response = actual_analytics_rules.upsert( + "company_analytics_rule", + { + "type": "popular_queries", + "params": { + "source": { + "collections": ["companies"], }, + "destination": {"collection": "companies_queries"}, }, + }, + ) + + assert response == { + "name": "company_analytics_rule", + "type": "popular_queries", + "params": { + "source": {"collections": ["companies"]}, + "destination": {"collection": "companies_queries"}, + }, + } + +def test_actual_retrieve( + actual_analytics_rules: AnalyticsRulesV1, + delete_all: None, + delete_all_analytics_rules_v1: None, + create_analytics_rule_v1: None, +) -> None: + """Test that the AnalyticsRulesV1 object can retrieve the rules from Typesense Server.""" + response = actual_analytics_rules.retrieve() + assert len(response["rules"]) == 1 + assert_match_object( + response["rules"][0], + { + "name": "company_analytics_rule", "params": { + "destination": {"collection": "companies_queries"}, + "limit": 1000, "source": {"collections": ["companies"]}, }, "type": "nohits_queries", + }, + ) + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncAnalyticsRulesV1 object is initialized correctly.""" + analytics_rules = AsyncAnalyticsRulesV1(fake_async_api_call) + + assert_match_object(analytics_rules.api_call, fake_async_api_call) + assert_object_lists_match( + analytics_rules.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + analytics_rules.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not analytics_rules.rules + + +def test_get_missing_analytics_rule_async( + fake_async_analytics_rules_v1: AsyncAnalyticsRulesV1, +) -> None: + """Test that the AsyncAnalyticsRulesV1 object can get a missing analytics_rule.""" + from typesense.async_analytics_rule_v1 import AsyncAnalyticsRuleV1 + + analytics_rule = fake_async_analytics_rules_v1["company_analytics_rule"] + + assert analytics_rule.rule_id == "company_analytics_rule" + assert_match_object(analytics_rule.api_call, fake_async_analytics_rules_v1.api_call) + assert_object_lists_match( + analytics_rule.api_call.node_manager.nodes, + fake_async_analytics_rules_v1.api_call.node_manager.nodes, + ) + assert_match_object( + analytics_rule.api_call.config.nearest_node, + fake_async_analytics_rules_v1.api_call.config.nearest_node, + ) + assert ( + analytics_rule._endpoint_path # noqa: WPS437 + == "/analytics/rules/company_analytics_rule" + ) + + +def test_get_existing_analytics_rule_async( + fake_async_analytics_rules_v1: AsyncAnalyticsRulesV1, +) -> None: + """Test that the AsyncAnalyticsRulesV1 object can get an existing analytics_rule.""" + analytics_rule = fake_async_analytics_rules_v1["company_analytics_rule"] + fetched_analytics_rule = fake_async_analytics_rules_v1["company_analytics_rule"] + + assert len(fake_async_analytics_rules_v1.rules) == 1 + + assert analytics_rule is fetched_analytics_rule +async def test_actual_create_async( + actual_async_analytics_rules_v1: AsyncAnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_collection: None, create_query_collection: None, ) -> None: + """Test that the AsyncAnalyticsRulesV1 object can create an analytics_rule on Typesense Server.""" + response = await actual_async_analytics_rules_v1.create( rule={ "name": "company_analytics_rule", "type": "nohits_queries", @@ -121,10 +243,14 @@ def test_get_existing_analytics_rule(fake_analytics_rules: AnalyticsRulesV1) -> } +async def test_actual_update_async( + actual_async_analytics_rules_v1: AsyncAnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_analytics_rule_v1: None, ) -> None: + """Test that the AsyncAnalyticsRulesV1 object can update an analytics_rule on Typesense Server.""" + response = await actual_async_analytics_rules_v1.upsert( "company_analytics_rule", { "type": "popular_queries", @@ -147,10 +273,14 @@ def test_get_existing_analytics_rule(fake_analytics_rules: AnalyticsRulesV1) -> } +async def test_actual_retrieve_async( + actual_async_analytics_rules_v1: AsyncAnalyticsRulesV1, delete_all: None, delete_all_analytics_rules_v1: None, create_analytics_rule_v1: None, ) -> None: + """Test that the AsyncAnalyticsRulesV1 object can retrieve the rules from Typesense Server.""" + response = await actual_async_analytics_rules_v1.retrieve() assert len(response["rules"]) == 1 assert_match_object( response["rules"][0], diff --git a/tests/analytics_v1_test.py b/tests/analytics_v1_test.py index f617b7b..5ca2f1d 100644 --- a/tests/analytics_v1_test.py +++ b/tests/analytics_v1_test.py @@ -6,6 +6,8 @@ from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.analytics_v1 import AnalyticsV1 from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_analytics_v1 import AsyncAnalyticsV1 @pytest.mark.skipif( @@ -34,3 +36,20 @@ def test_init(fake_api_call: ApiCall) -> None: ) assert not analytics.rules.rules + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncAnalyticsV1 object is initialized correctly.""" + analytics = AsyncAnalyticsV1(fake_async_api_call) + + assert_match_object(analytics.rules.api_call, fake_async_api_call) + assert_object_lists_match( + analytics.rules.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + analytics.rules.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not analytics.rules.rules diff --git a/tests/fixtures/analytics_rule_v1_fixtures.py b/tests/fixtures/analytics_rule_v1_fixtures.py index 0dca1d0..194ac2d 100644 --- a/tests/fixtures/analytics_rule_v1_fixtures.py +++ b/tests/fixtures/analytics_rule_v1_fixtures.py @@ -6,6 +6,9 @@ from typesense.analytics_rule_v1 import AnalyticsRuleV1 from typesense.analytics_rules_v1 import AnalyticsRulesV1 from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_analytics_rule_v1 import AsyncAnalyticsRuleV1 +from typesense.async_analytics_rules_v1 import AsyncAnalyticsRulesV1 @pytest.fixture(scope="function", name="delete_all_analytics_rules_v1") @@ -66,3 +69,21 @@ def actual_analytics_rules_v1_fixture(actual_api_call: ApiCall) -> AnalyticsRule def fake_analytics_rule_v1_fixture(fake_api_call: ApiCall) -> AnalyticsRuleV1: """Return a AnalyticsRule object with test values.""" return AnalyticsRuleV1(fake_api_call, "company_analytics_rule") + + +@pytest.fixture(scope="function", name="fake_async_analytics_rules_v1") +def fake_async_analytics_rules_v1_fixture(fake_async_api_call: AsyncApiCall) -> AsyncAnalyticsRulesV1: + """Return a AsyncAnalyticsRulesV1 object with test values.""" + return AsyncAnalyticsRulesV1(fake_async_api_call) + + +@pytest.fixture(scope="function", name="actual_async_analytics_rules_v1") +def actual_async_analytics_rules_v1_fixture(actual_async_api_call: AsyncApiCall) -> AsyncAnalyticsRulesV1: + """Return a AsyncAnalyticsRulesV1 object using a real API.""" + return AsyncAnalyticsRulesV1(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_analytics_rule_v1") +def fake_async_analytics_rule_v1_fixture(fake_async_api_call: AsyncApiCall) -> AsyncAnalyticsRuleV1: + """Return a AsyncAnalyticsRuleV1 object with test values.""" + return AsyncAnalyticsRuleV1(fake_async_api_call, "company_analytics_rule") From e6819073ad1c460b306c57799e977ea3adced5f9 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:45:40 +0200 Subject: [PATCH 13/32] refactor(collection): make TDoc typevar covariant in collection classes --- src/typesense/collection.py | 2 +- src/typesense/collections.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typesense/collection.py b/src/typesense/collection.py index a898656..688775f 100644 --- a/src/typesense/collection.py +++ b/src/typesense/collection.py @@ -35,7 +35,7 @@ from typesense.synonyms import Synonyms from typesense.types.document import DocumentSchema -TDoc = typing.TypeVar("TDoc", bound=DocumentSchema) +TDoc = typing.TypeVar("TDoc", bound=DocumentSchema, covariant=True) class Collection(typing.Generic[TDoc]): diff --git a/src/typesense/collections.py b/src/typesense/collections.py index dd9fe53..49f89cf 100644 --- a/src/typesense/collections.py +++ b/src/typesense/collections.py @@ -28,7 +28,7 @@ from typesense.types.collection import CollectionCreateSchema, CollectionSchema from typesense.types.document import DocumentSchema -TDoc = typing.TypeVar("TDoc", bound=DocumentSchema) +TDoc = typing.TypeVar("TDoc", bound=DocumentSchema, covariant=True) class Collections(typing.Generic[TDoc]): From fb20e8933d0a2dc7366087ef48504b9573e4b75e Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:47:00 +0200 Subject: [PATCH 14/32] feat(collection): add async support for collection operations - add AsyncCollection class for async individual collection operations - add AsyncCollections class for async collection collection operations - add async tests for collection and collections functionality - add async fixtures for testing async collection operations - remove future annotations imports from test files --- src/typesense/async_collection.py | 160 ++++++++++++++++++++++++ src/typesense/async_collections.py | 163 +++++++++++++++++++++++++ tests/collection_test.py | 2 - tests/collections_test.py | 160 +++++++++++++++++++++++- tests/fixtures/collections_fixtures.py | 25 ++++ 5 files changed, 507 insertions(+), 3 deletions(-) create mode 100644 src/typesense/async_collection.py create mode 100644 src/typesense/async_collections.py diff --git a/src/typesense/async_collection.py b/src/typesense/async_collection.py new file mode 100644 index 0000000..206c783 --- /dev/null +++ b/src/typesense/async_collection.py @@ -0,0 +1,160 @@ +""" +This module provides async functionality for managing individual collections in the Typesense API. + +It contains the AsyncCollection class, which allows for retrieving, updating, and deleting +collections asynchronously. + +Classes: + AsyncCollection: Manages async operations on a single collection in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.types.collection: Provides CollectionSchema and CollectionUpdateSchema types. + - typesense.types.document: Provides DocumentSchema type. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +from typing_extensions import deprecated + +from typesense.types.collection import CollectionSchema, CollectionUpdateSchema + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.async_documents import AsyncDocuments +from typesense.async_overrides import AsyncOverrides +from typesense.async_synonyms import AsyncSynonyms +from typesense.types.document import DocumentSchema + +TDoc = typing.TypeVar("TDoc", bound=DocumentSchema, covariant=True) + + +class AsyncCollection(typing.Generic[TDoc]): + """ + Manages async operations on a single collection in the Typesense API. + + This class provides async methods to retrieve, update, and delete a collection. + It is generic over the document type TDoc, which should be a subtype of DocumentSchema. + + Attributes: + name (str): The name of the collection. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + def __init__(self, api_call: AsyncApiCall, name: str): + """ + Initialize the AsyncCollection instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + name (str): The name of the collection. + """ + self.name = name + self.api_call = api_call + + self.documents: AsyncDocuments[TDoc] = AsyncDocuments(api_call, name) + self._overrides = AsyncOverrides(api_call, name) + self._synonyms = AsyncSynonyms(api_call, name) + + async def retrieve(self) -> CollectionSchema: + """ + Retrieve the schema of this collection from Typesense. + + Returns: + CollectionSchema: The schema of the collection. + """ + response: CollectionSchema = await self.api_call.get( + endpoint=self._endpoint_path, + entity_type=CollectionSchema, + as_json=True, + ) + return response + + async def update( + self, schema_change: CollectionUpdateSchema + ) -> CollectionUpdateSchema: + """ + Update the schema of this collection in Typesense. + + Args: + schema_change (CollectionUpdateSchema): + The changes to apply to the collection schema. + + Returns: + CollectionUpdateSchema: The updated schema of the collection. + """ + response: CollectionUpdateSchema = await self.api_call.patch( + endpoint=self._endpoint_path, + body=schema_change, + entity_type=CollectionUpdateSchema, + ) + return response + + async def delete( + self, + delete_parameters: typing.Union[ + typing.Dict[str, typing.Union[str, bool]], + None, + ] = None, + ) -> CollectionSchema: + """ + Delete this collection from Typesense. + + Args: + delete_parameters (Union[Dict[str, Union[str, bool]], None], optional): + Additional parameters for the delete operation. Defaults to None. + + Returns: + CollectionSchema: The schema of the deleted collection. + """ + response: CollectionSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=CollectionSchema, + params=delete_parameters, + ) + return response + + @property + @deprecated( + "Overrides is deprecated on v30+. Use client.curation_sets instead.", + category=None, + ) + def overrides(self) -> AsyncOverrides: + """Return the AsyncOverrides instance for this collection. + + Returns: + AsyncOverrides: The AsyncOverrides instance for this collection. + """ + return self._overrides + + @property + @deprecated( + "Synonyms is deprecated on v30+. Use client.synonym_sets instead.", + category=None, + ) + def synonyms(self) -> AsyncSynonyms: + """Return the AsyncSynonyms instance for this collection. + + Returns: + AsyncSynonyms: The AsyncSynonyms instance for this collection. + """ + """Return the AsyncSynonyms instance for this collection.""" + return self._synonyms + + @property + def _endpoint_path(self) -> str: + """ + Get the API endpoint path for this collection. + + Returns: + str: The full endpoint path for the collection. + """ + from typesense.async_collections import AsyncCollections + + return "/".join([AsyncCollections.resource_path, self.name]) diff --git a/src/typesense/async_collections.py b/src/typesense/async_collections.py new file mode 100644 index 0000000..06fce80 --- /dev/null +++ b/src/typesense/async_collections.py @@ -0,0 +1,163 @@ +""" +This module provides async functionality for managing collections in the Typesense API. + +It contains the AsyncCollections class, which allows for creating, retrieving, and +accessing individual collections asynchronously. + +Classes: + AsyncCollections: Manages collections in the Typesense API (async). + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.async_collection: Provides the AsyncCollection class for individual collection operations. + - typesense.types.collection: Provides CollectionCreateSchema and CollectionSchema types. + - typesense.types.document: Provides DocumentSchema type. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.async_collection import AsyncCollection +from typesense.types.collection import CollectionCreateSchema, CollectionSchema +from typesense.types.document import DocumentSchema + +TDoc = typing.TypeVar("TDoc", bound=DocumentSchema, covariant=True) + + +class AsyncCollections(typing.Generic[TDoc]): + """ + Manages collections in the Typesense API (async). + + This class provides async methods to create, retrieve, and access individual collections. + It is generic over the document type TDoc, which should be a subtype of DocumentSchema. + + Attributes: + resource_path (str): The API endpoint path for collections operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + collections (Dict[str, AsyncCollection[TDoc]]): + A dictionary of AsyncCollection instances, keyed by collection name. + """ + + resource_path: typing.Final[str] = "/collections" + + def __init__(self, api_call: AsyncApiCall): + """ + Initialize the AsyncCollections instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + self.collections: typing.Dict[str, AsyncCollection[TDoc]] = {} + + async def __contains__(self, collection_name: str) -> bool: + """ + Check if a collection exists in Typesense. + + This method tries to retrieve the specified collection to check for its existence, + utilizing the AsyncCollection.retrieve() method but without caching non-existent collections. + + Args: + collection_name (str): The name of the collection to check. + + Returns: + bool: True if the collection exists, False otherwise. + """ + if collection_name in self.collections: + try: + await self.collections[collection_name].retrieve() + return True + except Exception: + self.collections.pop(collection_name, None) + return False + + try: + await AsyncCollection(self.api_call, collection_name).retrieve() + return True + except Exception: + return False + + def __getitem__(self, collection_name: str) -> AsyncCollection[TDoc]: + """ + Get or create an AsyncCollection instance for a given collection name. + + This method allows accessing collections using dictionary-like syntax. + If the AsyncCollection instance doesn't exist, it creates a new one. + + Args: + collection_name (str): The name of the collection to access. + + Returns: + AsyncCollection[TDoc]: The AsyncCollection instance for the specified collection name. + + Example: + >>> collections = AsyncCollections(async_api_call) + >>> fruits_collection = collections["fruits"] + """ + if not self.collections.get(collection_name): + self.collections[collection_name] = AsyncCollection( + self.api_call, + collection_name, + ) + return self.collections[collection_name] + + async def create(self, schema: CollectionCreateSchema) -> CollectionSchema: + """ + Create a new collection in Typesense. + + Args: + schema (CollectionCreateSchema): + The schema defining the structure of the new collection. + + Returns: + CollectionSchema: + The schema of the created collection, as returned by the API. + + Example: + >>> collections = AsyncCollections(async_api_call) + >>> schema = { + ... "name": "companies", + ... "fields": [ + ... {"name": "company_name", "type": "string"}, + ... {"name": "num_employees", "type": "int32"}, + ... {"name": "country", "type": "string", "facet": True}, + ... ], + ... "default_sorting_field": "num_employees", + ... } + >>> created_schema = await collections.create(schema) + """ + call: CollectionSchema = await self.api_call.post( + endpoint=AsyncCollections.resource_path, + entity_type=CollectionSchema, + as_json=True, + body=schema, + ) + return call + + async def retrieve(self) -> typing.List[CollectionSchema]: + """ + Retrieve all collections from Typesense. + + Returns: + List[CollectionSchema]: + A list of schemas for all collections in the Typesense instance. + + Example: + >>> collections = AsyncCollections(async_api_call) + >>> all_collections = await collections.retrieve() + >>> for collection in all_collections: + ... print(collection["name"]) + """ + call: typing.List[CollectionSchema] = await self.api_call.get( + endpoint=AsyncCollections.resource_path, + as_json=True, + entity_type=typing.List[CollectionSchema], + ) + return call diff --git a/tests/collection_test.py b/tests/collection_test.py index d2cccfd..8ea5adc 100644 --- a/tests/collection_test.py +++ b/tests/collection_test.py @@ -1,7 +1,5 @@ """Tests for the Collection class.""" -from __future__ import annotations - from tests.utils.object_assertions import ( assert_match_object, assert_object_lists_match, diff --git a/tests/collections_test.py b/tests/collections_test.py index 8e3d7ef..60b4d5d 100644 --- a/tests/collections_test.py +++ b/tests/collections_test.py @@ -4,7 +4,8 @@ import sys -import requests_mock +from typesense.async_api_call import AsyncApiCall + if sys.version_info >= (3, 11): import typing @@ -14,6 +15,7 @@ from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.api_call import ApiCall from typesense.collections import Collections +from typesense.async_collections import AsyncCollections from typesense.types.collection import CollectionSchema @@ -33,6 +35,22 @@ def test_init(fake_api_call: ApiCall) -> None: assert not collections.collections +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the Collections object is initialized correctly.""" + collections = AsyncCollections(fake_async_api_call) + + assert_match_object(collections.api_call, fake_async_api_call) + assert_object_lists_match( + collections.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + collections.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert not collections.collections + + def test_get_missing_collection(fake_collections: Collections) -> None: """Test that the Collections object can get a missing collection.""" collection = fake_collections["companies"] @@ -51,6 +69,24 @@ def test_get_missing_collection(fake_collections: Collections) -> None: assert collection._endpoint_path == "/collections/companies" # noqa: WPS437 +def test_get_missing_collection_async(fake_async_collections: Collections) -> None: + """Test that the Collections object can get a missing collection.""" + collection = fake_async_collections["companies"] + + assert collection.name == "companies" + assert_match_object(collection.api_call, fake_async_collections.api_call) + assert_object_lists_match( + collection.api_call.node_manager.nodes, + fake_async_collections.api_call.node_manager.nodes, + ) + assert_match_object( + collection.api_call.config.nearest_node, + fake_async_collections.api_call.config.nearest_node, + ) + assert collection.overrides.collection_name == "companies" + assert collection._endpoint_path == "/collections/companies" # noqa: WPS437 + + def test_get_existing_collection(fake_collections: Collections) -> None: """Test that the Collections object can get an existing collection.""" collection = fake_collections["companies"] @@ -194,3 +230,125 @@ def test_actual_contains( assert "non_existent_collection" not in actual_collections # Test again assert "non_existent_collection" not in actual_collections + + +async def test_actual_create_async( + actual_async_collections: AsyncCollections, delete_all: None +) -> None: + """Test that the Collections object can create a collection on Typesense Server.""" + expected: CollectionSchema = { + "default_sorting_field": "", + "enable_nested_fields": False, + "fields": [ + { + "name": "company_name", + "type": "string", + "facet": False, + "index": True, + "optional": False, + "locale": "", + "sort": False, + "infix": False, + "stem": False, + "stem_dictionary": "", + "truncate_len": 100, + "store": True, + }, + { + "name": "num_employees", + "type": "int32", + "facet": False, + "index": True, + "optional": False, + "locale": "", + "sort": False, + "infix": False, + "stem": False, + "stem_dictionary": "", + "truncate_len": 100, + "store": True, + }, + ], + "name": "companies", + "num_documents": 0, + "symbols_to_index": [], + "token_separators": [], + "synonym_sets": [], + "curation_sets": [], + } + + response = await actual_async_collections.create( + { + "name": "companies", + "fields": [ + { + "name": "company_name", + "type": "string", + }, + { + "name": "num_employees", + "type": "int32", + "sort": False, + }, + ], + }, + ) + + response.pop("created_at") + + assert response == expected + + +async def test_actual_retrieve_async( + actual_async_collections: AsyncCollections, + delete_all: None, + create_collection: None, +) -> None: + """Test that the Collections object can retrieve collections.""" + response = await actual_async_collections.retrieve() + + expected: typing.List[CollectionSchema] = [ + { + "default_sorting_field": "num_employees", + "enable_nested_fields": False, + "fields": [ + { + "name": "company_name", + "type": "string", + "facet": False, + "index": True, + "optional": False, + "locale": "", + "sort": False, + "infix": False, + "stem": False, + "stem_dictionary": "", + "truncate_len": 100, + "store": True, + }, + { + "name": "num_employees", + "type": "int32", + "facet": False, + "index": True, + "optional": False, + "locale": "", + "sort": True, + "infix": False, + "stem": False, + "stem_dictionary": "", + "truncate_len": 100, + "store": True, + }, + ], + "name": "companies", + "num_documents": 0, + "symbols_to_index": [], + "token_separators": [], + "synonym_sets": [], + "curation_sets": [], + }, + ] + + response[0].pop("created_at") + assert response == expected diff --git a/tests/fixtures/collections_fixtures.py b/tests/fixtures/collections_fixtures.py index de75eea..4fb2e9e 100644 --- a/tests/fixtures/collections_fixtures.py +++ b/tests/fixtures/collections_fixtures.py @@ -4,6 +4,9 @@ import requests from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_collection import AsyncCollection +from typesense.async_collections import AsyncCollections from typesense.collection import Collection from typesense.collections import Collections @@ -81,13 +84,35 @@ def actual_collections_fixture(actual_api_call: ApiCall) -> Collections: return Collections(actual_api_call) +@pytest.fixture(scope="function", name="actual_async_collections") +def actual_async_collections_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncCollections: + """Return a Collections object using a real API.""" + return AsyncCollections(actual_async_api_call) + + @pytest.fixture(scope="function", name="fake_collections") def fake_collections_fixture(fake_api_call: ApiCall) -> Collections: """Return a Collections object with test values.""" return Collections(fake_api_call) +@pytest.fixture(scope="function", name="fake_async_collections") +def fake_collections_async_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncCollections: + """Return a Collections object with test values.""" + return AsyncCollections(fake_async_api_call) + + @pytest.fixture(scope="function", name="fake_collection") def fake_collection_fixture(fake_api_call: ApiCall) -> Collection: """Return a Collection object with test values.""" return Collection(fake_api_call, "companies") + + +@pytest.fixture(scope="function", name="fake_async_collection") +def fake_async_collection_fixture(fake_async_api_call: AsyncApiCall) -> AsyncCollection: + """Return a Collection object with test values.""" + return AsyncCollection(fake_async_api_call, "companies") From 421608aba5fb52e05344ef07e512dc1c15d351fe Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:47:45 +0200 Subject: [PATCH 15/32] feat(convo): add async support for conversation model operations - add AsyncConversationModel class for async individual model operations - add AsyncConversationsModels class for async models collection operations - add async tests for conversation model and models functionality - add async fixtures for testing async conversation model operations --- src/typesense/async_conversation_model.py | 104 ++++++++++++++ src/typesense/async_conversations_models.py | 131 ++++++++++++++++++ tests/conversation_model_test.py | 103 ++++++++++++++ tests/conversations_models_test.py | 101 ++++++++++++++ tests/fixtures/conversation_model_fixtures.py | 27 ++++ 5 files changed, 466 insertions(+) create mode 100644 src/typesense/async_conversation_model.py create mode 100644 src/typesense/async_conversations_models.py diff --git a/src/typesense/async_conversation_model.py b/src/typesense/async_conversation_model.py new file mode 100644 index 0000000..abee3ab --- /dev/null +++ b/src/typesense/async_conversation_model.py @@ -0,0 +1,104 @@ +""" +This module provides async functionality for managing individual conversation models in Typesense. + +It contains the AsyncConversationModel class, which allows for retrieving, updating, and deleting +conversation models asynchronously. + +Classes: + AsyncConversationModel: Manages async operations on a single conversation model in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.types.conversations_model: Provides ConversationModelCreateSchema, ConversationModelDeleteSchema, and ConversationModelSchema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.types.conversations_model import ( + ConversationModelCreateSchema, + ConversationModelDeleteSchema, + ConversationModelSchema, +) + + +class AsyncConversationModel: + """ + Manages async operations on a single conversation model in the Typesense API. + + This class provides async methods to retrieve, update, and delete a conversation model. + + Attributes: + model_id (str): The ID of the conversation model. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + def __init__(self, api_call: AsyncApiCall, model_id: str) -> None: + """ + Initialize the AsyncConversationModel instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + model_id (str): The ID of the conversation model. + """ + self.model_id = model_id + self.api_call = api_call + + async def retrieve(self) -> ConversationModelSchema: + """ + Retrieve this specific conversation model. + + Returns: + ConversationModelSchema: The schema containing the conversation model details. + """ + response: ConversationModelSchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=ConversationModelSchema, + ) + return response + + async def update( + self, model: ConversationModelCreateSchema + ) -> ConversationModelSchema: + """ + Update this specific conversation model. + + Args: + model (ConversationModelCreateSchema): + The schema containing the updated model details. + + Returns: + ConversationModelSchema: The schema containing the updated conversation model. + """ + response: ConversationModelSchema = await self.api_call.put( + self._endpoint_path, + body=model, + entity_type=ConversationModelSchema, + ) + return response + + async def delete(self) -> ConversationModelDeleteSchema: + """ + Delete this specific conversation model. + + Returns: + ConversationModelDeleteSchema: The schema containing the deletion response. + """ + response: ConversationModelDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=ConversationModelDeleteSchema, + ) + return response + + @property + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific conversation model. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_conversations_models import AsyncConversationsModels + + return "/".join([AsyncConversationsModels.resource_path, self.model_id]) diff --git a/src/typesense/async_conversations_models.py b/src/typesense/async_conversations_models.py new file mode 100644 index 0000000..532196c --- /dev/null +++ b/src/typesense/async_conversations_models.py @@ -0,0 +1,131 @@ +""" +This module provides async functionality for managing conversation models in Typesense. + +It contains the AsyncConversationsModels class, which allows for creating, retrieving, and +accessing individual conversation models asynchronously. + +Classes: + AsyncConversationsModels: Manages conversation models in the Typesense API (async). + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.async_conversation_model: Provides the AsyncConversationModel class for individual conversation model operations. + - typesense.types.conversations_model: Provides ConversationModelCreateSchema and ConversationModelSchema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +from typesense.async_api_call import AsyncApiCall +from typesense.async_conversation_model import AsyncConversationModel +from typesense.types.conversations_model import ( + ConversationModelCreateSchema, + ConversationModelSchema, +) + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + + +class AsyncConversationsModels: + """ + Manages conversation models in the Typesense API (async). + + This class provides async methods to create, retrieve, and access individual conversation models. + + Attributes: + resource_path (str): The API endpoint path for conversation models operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + conversations_models (Dict[str, AsyncConversationModel]): + A dictionary of AsyncConversationModel instances, keyed by model ID. + """ + + resource_path: typing.Final[str] = "/conversations/models" + + def __init__(self, api_call: AsyncApiCall) -> None: + """ + Initialize the AsyncConversationsModels instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + self.conversations_models: typing.Dict[str, AsyncConversationModel] = {} + + def __getitem__(self, model_id: str) -> AsyncConversationModel: + """ + Get or create an AsyncConversationModel instance for a given model ID. + + This method allows accessing conversation models using dictionary-like syntax. + If the AsyncConversationModel instance doesn't exist, it creates a new one. + + Args: + model_id (str): The ID of the conversation model. + + Returns: + AsyncConversationModel: The AsyncConversationModel instance for the specified model ID. + + Example: + >>> conversations_models = AsyncConversationsModels(async_api_call) + >>> model = conversations_models["model_id"] + """ + if model_id not in self.conversations_models: + self.conversations_models[model_id] = AsyncConversationModel( + self.api_call, + model_id, + ) + return self.conversations_models[model_id] + + async def create( + self, model: ConversationModelCreateSchema + ) -> ConversationModelSchema: + """ + Create a new conversation model. + + Args: + model (ConversationModelCreateSchema): + The schema for creating the conversation model. + + Returns: + ConversationModelSchema: The created conversation model. + + Example: + >>> conversations_models = AsyncConversationsModels(async_api_call) + >>> model = await conversations_models.create( + ... { + ... "api_key": "key", + ... "model_name": "openai/gpt-3.5-turbo", + ... "history_collection": "conversation_store", + ... } + ... ) + """ + response: ConversationModelSchema = await self.api_call.post( + endpoint=AsyncConversationsModels.resource_path, + entity_type=ConversationModelSchema, + as_json=True, + body=model, + ) + return response + + async def retrieve(self) -> typing.List[ConversationModelSchema]: + """ + Retrieve all conversation models. + + Returns: + List[ConversationModelSchema]: A list of all conversation models. + + Example: + >>> conversations_models = AsyncConversationsModels(async_api_call) + >>> all_models = await conversations_models.retrieve() + >>> for model in all_models: + ... print(model["id"]) + """ + response: typing.List[ConversationModelSchema] = await self.api_call.get( + endpoint=AsyncConversationsModels.resource_path, + entity_type=typing.List[ConversationModelSchema], + as_json=True, + ) + return response diff --git a/tests/conversation_model_test.py b/tests/conversation_model_test.py index ead4817..5c2d2dd 100644 --- a/tests/conversation_model_test.py +++ b/tests/conversation_model_test.py @@ -11,6 +11,9 @@ assert_to_contain_keys, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_conversation_model import AsyncConversationModel +from typesense.async_conversations_models import AsyncConversationsModels from typesense.conversation_model import ConversationModel from typesense.conversations_models import ConversationsModels from typesense.types.conversations_model import ( @@ -44,6 +47,29 @@ def test_init(fake_api_call: ApiCall) -> None: ) +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncConversationModel object is initialized correctly.""" + conversation_model = AsyncConversationModel( + fake_async_api_call, + "conversation_model_id", + ) + + assert conversation_model.model_id == "conversation_model_id" + assert_match_object(conversation_model.api_call, fake_async_api_call) + assert_object_lists_match( + conversation_model.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + conversation_model.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert ( + conversation_model._endpoint_path # noqa: WPS437 + == "/conversations/models/conversation_model_id" + ) + + @pytest.mark.open_ai def test_actual_retrieve( actual_conversations_models: ConversationsModels, @@ -113,3 +139,80 @@ def test_actual_delete( assert response.get("system_prompt") == "This is a system prompt" assert response.get("id") == create_conversations_model assert response.get("id") == create_conversations_model + + +@pytest.mark.open_ai +async def test_actual_retrieve_async( + actual_async_conversations_models: AsyncConversationsModels, + delete_all_conversations_models: None, + create_conversations_model: str, +) -> None: + """Test it can retrieve a conversation_model from Typesense Server.""" + response = await actual_async_conversations_models[ + create_conversations_model + ].retrieve() + + assert_to_contain_keys( + response, + ["id", "model_name", "system_prompt", "max_bytes", "api_key"], + ) + assert response.get("id") == create_conversations_model + + +@pytest.mark.open_ai +async def test_actual_update_async( + actual_async_conversations_models: AsyncConversationsModels, + delete_all_conversations_models: None, + create_conversations_model: str, +) -> None: + """Test that it can update a conversation_model from Typesense Server.""" + response = await actual_async_conversations_models[ + create_conversations_model + ].update( + {"system_prompt": "This is a new system prompt"}, + ) + + assert_to_contain_keys( + response, + [ + "id", + "model_name", + "system_prompt", + "max_bytes", + "api_key", + "ttl", + "history_collection", + ], + ) + + assert response.get("system_prompt") == "This is a new system prompt" + assert response.get("id") == create_conversations_model + + +@pytest.mark.open_ai +async def test_actual_delete_async( + actual_async_conversations_models: AsyncConversationsModels, + delete_all_conversations_models: None, + create_conversations_model: str, +) -> None: + """Test that it can delete an conversation_model from Typesense Server.""" + response = await actual_async_conversations_models[ + create_conversations_model + ].delete() + + assert_to_contain_keys( + response, + [ + "id", + "model_name", + "system_prompt", + "max_bytes", + "api_key", + "ttl", + "history_collection", + ], + ) + + assert response.get("system_prompt") == "This is a system prompt" + assert response.get("id") == create_conversations_model + assert response.get("id") == create_conversations_model diff --git a/tests/conversations_models_test.py b/tests/conversations_models_test.py index cf76e04..7c40498 100644 --- a/tests/conversations_models_test.py +++ b/tests/conversations_models_test.py @@ -19,6 +19,8 @@ assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_conversations_models import AsyncConversationsModels from typesense.conversations_models import ConversationsModels from typesense.types.conversations_model import ConversationModelSchema @@ -40,6 +42,23 @@ def test_init(fake_api_call: ApiCall) -> None: assert not conversations_models.conversations_models +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncConversationsModels object is initialized correctly.""" + conversations_models = AsyncConversationsModels(fake_async_api_call) + + assert_match_object(conversations_models.api_call, fake_async_api_call) + assert_object_lists_match( + conversations_models.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + conversations_models.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not conversations_models.conversations_models + + def test_get_missing_conversations_model( fake_conversations_models: ConversationsModels, ) -> None: @@ -64,6 +83,30 @@ def test_get_missing_conversations_model( ) +def test_get_missing_conversations_model_async( + fake_async_conversations_models: AsyncConversationsModels, +) -> None: + """Test that the AsyncConversationsModels object can get a missing conversations_model.""" + conversations_model = fake_async_conversations_models["conversation_model_id"] + + assert_match_object( + conversations_model.api_call, + fake_async_conversations_models.api_call, + ) + assert_object_lists_match( + conversations_model.api_call.node_manager.nodes, + fake_async_conversations_models.api_call.node_manager.nodes, + ) + assert_match_object( + conversations_model.api_call.config.nearest_node, + fake_async_conversations_models.api_call.config.nearest_node, + ) + assert ( + conversations_model._endpoint_path # noqa: WPS437 + == "/conversations/models/conversation_model_id" + ) + + def test_get_existing_conversations_model( fake_conversations_models: ConversationsModels, ) -> None: @@ -76,6 +119,20 @@ def test_get_existing_conversations_model( assert conversations_model is fetched_conversations_model +def test_get_existing_conversations_model_async( + fake_async_conversations_models: AsyncConversationsModels, +) -> None: + """Test that the AsyncConversationsModels object can get an existing conversations_model.""" + conversations_model = fake_async_conversations_models["conversations_model_id"] + fetched_conversations_model = fake_async_conversations_models[ + "conversations_model_id" + ] + + assert len(fake_async_conversations_models.conversations_models) == 1 + + assert conversations_model is fetched_conversations_model + + @pytest.mark.open_ai def test_actual_create( actual_conversations_models: ConversationsModels, @@ -118,3 +175,47 @@ def test_actual_retrieve( response[0], ["id", "api_key", "max_bytes", "model_name", "system_prompt"], ) + + +@pytest.mark.open_ai +async def test_actual_create_async( + actual_async_conversations_models: AsyncConversationsModels, + create_conversation_history_collection: None, +) -> None: + """Test that it can create an conversations_model on Typesense Server.""" + response = await actual_async_conversations_models.create( + { + "api_key": os.environ["OPEN_AI_KEY"], + "history_collection": "conversation_store", + "max_bytes": 16384, + "model_name": "openai/gpt-3.5-turbo", + "system_prompt": "This is meant for testing purposes", + }, + ) + + assert_to_contain_keys( + response, + ["id", "api_key", "max_bytes", "model_name", "system_prompt"], + ) + + +@pytest.mark.open_ai +async def test_actual_retrieve_async( + actual_async_conversations_models: AsyncConversationsModels, + delete_all: None, + delete_all_conversations_models: None, + create_conversations_model: str, +) -> None: + """Test that it can retrieve an conversations_model from Typesense Server.""" + response = await actual_async_conversations_models.retrieve() + assert len(response) == 1 + assert_to_contain_object( + response[0], + { + "id": create_conversations_model, + }, + ) + assert_to_contain_keys( + response[0], + ["id", "api_key", "max_bytes", "model_name", "system_prompt"], + ) diff --git a/tests/fixtures/conversation_model_fixtures.py b/tests/fixtures/conversation_model_fixtures.py index 03451e7..75e1fce 100644 --- a/tests/fixtures/conversation_model_fixtures.py +++ b/tests/fixtures/conversation_model_fixtures.py @@ -7,6 +7,9 @@ from dotenv import load_dotenv from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_conversation_model import AsyncConversationModel +from typesense.async_conversations_models import AsyncConversationsModels from typesense.conversation_model import ConversationModel from typesense.conversations_models import ConversationsModels @@ -81,6 +84,30 @@ def actual_conversations_models_fixture( return ConversationsModels(actual_api_call) +@pytest.fixture(scope="function", name="actual_async_conversations_models") +def actual_async_conversations_models_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncConversationsModels: + """Return a AsyncConversationsModels object using a real API.""" + return AsyncConversationsModels(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_conversations_models") +def fake_async_conversations_models_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncConversationsModels: + """Return a AsyncConversationsModels object with test values.""" + return AsyncConversationsModels(fake_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_conversation_model") +def fake_async_conversation_model_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncConversationModel: + """Return a AsyncConversationModel object with test values.""" + return AsyncConversationModel(fake_async_api_call, "conversation_model_id") + + @pytest.fixture(scope="function", name="create_conversation_history_collection") def create_conversation_history_collection_fixture() -> None: """Create a collection for conversation history in the Typesense server.""" From e81edecfcc26a9781f4c73b1c91463898bf029a6 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:48:17 +0200 Subject: [PATCH 16/32] feat(curation): add async support for curation set operations - add AsyncCurationSet class for async individual curation set operations - add AsyncCurationSets class for async curation sets collection operations - add async tests for curation set and sets functionality - add async fixtures for testing async curation set operations - remove future annotations imports from test files --- src/typesense/async_curation_set.py | 211 ++++++++++++++++++++++++ src/typesense/async_curation_sets.py | 91 ++++++++++ tests/curation_set_test.py | 164 ++++++++++++++++++ tests/curation_sets_test.py | 78 ++++++++- tests/fixtures/curation_set_fixtures.py | 27 +++ 5 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 src/typesense/async_curation_set.py create mode 100644 src/typesense/async_curation_sets.py diff --git a/src/typesense/async_curation_set.py b/src/typesense/async_curation_set.py new file mode 100644 index 0000000..a31ea2d --- /dev/null +++ b/src/typesense/async_curation_set.py @@ -0,0 +1,211 @@ +""" +This module provides async functionality for managing individual curation sets in Typesense. + +It contains the AsyncCurationSet class, which allows for retrieving, updating, deleting, +and managing items within a curation set asynchronously. + +Classes: + AsyncCurationSet: Manages async operations on a single curation set in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.types.curation_set: Provides various curation set schema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.types.curation_set import ( + CurationItemDeleteSchema, + CurationItemSchema, + CurationSetDeleteSchema, + CurationSetListItemResponseSchema, + CurationSetSchema, + CurationSetUpsertSchema, +) + + +class AsyncCurationSet: + """ + Manages async operations on a single curation set in the Typesense API. + + This class provides async methods to retrieve, update, and delete a curation set, + as well as manage items within the curation set. + + Attributes: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + name (str): The name of the curation set. + """ + + def __init__(self, api_call: AsyncApiCall, name: str) -> None: + """ + Initialize the AsyncCurationSet instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + name (str): The name of the curation set. + """ + self.api_call = api_call + self.name = name + + @property + def _endpoint_path(self) -> str: + """ + Get the API endpoint path for this curation set. + + Returns: + str: The full endpoint path for the curation set. + """ + from typesense.async_curation_sets import AsyncCurationSets + + return "/".join([AsyncCurationSets.resource_path, self.name]) + + async def retrieve(self) -> CurationSetSchema: + """ + Retrieve this specific curation set. + + Returns: + CurationSetSchema: The schema containing the curation set details. + """ + response: CurationSetSchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=CurationSetSchema, + ) + return response + + async def delete(self) -> CurationSetDeleteSchema: + """ + Delete this specific curation set. + + Returns: + CurationSetDeleteSchema: The schema containing the deletion response. + """ + response: CurationSetDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=CurationSetDeleteSchema, + ) + return response + + async def upsert( + self, + payload: CurationSetUpsertSchema, + ) -> CurationSetSchema: + """ + Create or update this curation set. + + Args: + payload (CurationSetUpsertSchema): The schema for creating or updating the curation set. + + Returns: + CurationSetSchema: The created or updated curation set. + """ + response: CurationSetSchema = await self.api_call.put( + "/".join([self._endpoint_path]), + body=payload, + entity_type=CurationSetSchema, + ) + return response + + # Items sub-resource + @property + def _items_path(self) -> str: + """ + Get the API endpoint path for items in this curation set. + + Returns: + str: The full endpoint path for items (e.g., /curation_sets/{name}/items). + """ + return "/".join([self._endpoint_path, "items"]) + + async def list_items( + self, + *, + limit: typing.Union[int, None] = None, + offset: typing.Union[int, None] = None, + ) -> CurationSetListItemResponseSchema: + """ + List items in this curation set. + + Args: + limit (Union[int, None], optional): Maximum number of items to return. Defaults to None. + offset (Union[int, None], optional): Number of items to skip. Defaults to None. + + Returns: + CurationSetListItemResponseSchema: The list of items in the curation set. + """ + params: typing.Dict[str, typing.Union[int, None]] = { + "limit": limit, + "offset": offset, + } + # Filter out None values to avoid sending them + clean_params: typing.Dict[str, int] = { + k: v for k, v in params.items() if v is not None + } + response: CurationSetListItemResponseSchema = await self.api_call.get( + self._items_path, + as_json=True, + entity_type=CurationSetListItemResponseSchema, + params=clean_params or None, + ) + return response + + async def get_item(self, item_id: str) -> CurationItemSchema: + """ + Get a specific item from this curation set. + + Args: + item_id (str): The ID of the item to retrieve. + + Returns: + CurationItemSchema: The item schema. + """ + response: CurationItemSchema = await self.api_call.get( + "/".join([self._items_path, item_id]), + as_json=True, + entity_type=CurationItemSchema, + ) + return response + + async def upsert_item( + self, item_id: str, item: CurationItemSchema + ) -> CurationItemSchema: + """ + Create or update an item in this curation set. + + Args: + item_id (str): The ID of the item. + item (CurationItemSchema): The item schema. + + Returns: + CurationItemSchema: The created or updated item. + """ + response: CurationItemSchema = await self.api_call.put( + "/".join([self._items_path, item_id]), + body=item, + entity_type=CurationItemSchema, + ) + return response + + async def delete_item(self, item_id: str) -> CurationItemDeleteSchema: + """ + Delete an item from this curation set. + + Args: + item_id (str): The ID of the item to delete. + + Returns: + CurationItemDeleteSchema: The deletion response. + """ + response: CurationItemDeleteSchema = await self.api_call.delete( + "/".join([self._items_path, item_id]), + entity_type=CurationItemDeleteSchema, + ) + return response diff --git a/src/typesense/async_curation_sets.py b/src/typesense/async_curation_sets.py new file mode 100644 index 0000000..370c1b9 --- /dev/null +++ b/src/typesense/async_curation_sets.py @@ -0,0 +1,91 @@ +""" +This module provides async functionality for managing curation sets in Typesense. + +It contains the AsyncCurationSets class, which allows for retrieving and +accessing individual curation sets asynchronously. + +Classes: + AsyncCurationSets: Manages curation sets in the Typesense API (async). + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.async_curation_set: Provides the AsyncCurationSet class for individual curation set operations. + - typesense.types.curation_set: Provides CurationSetsListResponseSchema type. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.async_curation_set import AsyncCurationSet +from typesense.types.curation_set import CurationSetsListResponseSchema + + +class AsyncCurationSets: + """ + Manages curation sets in the Typesense API (async). + + This class provides async methods to retrieve and access individual curation sets. + + Attributes: + resource_path (str): The API endpoint path for curation sets operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + resource_path: typing.Final[str] = "/curation_sets" + + def __init__(self, api_call: AsyncApiCall) -> None: + """ + Initialize the AsyncCurationSets instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + + async def retrieve(self) -> CurationSetsListResponseSchema: + """ + Retrieve all curation sets. + + Returns: + CurationSetsListResponseSchema: The list of all curation sets. + + Example: + >>> curation_sets = AsyncCurationSets(async_api_call) + >>> all_sets = await curation_sets.retrieve() + >>> for set in all_sets: + ... print(set["name"]) + """ + response: CurationSetsListResponseSchema = await self.api_call.get( + AsyncCurationSets.resource_path, + as_json=True, + entity_type=CurationSetsListResponseSchema, + ) + return response + + def __getitem__(self, curation_set_name: str) -> AsyncCurationSet: + """ + Get or create an AsyncCurationSet instance for a given curation set name. + + This method allows accessing curation sets using dictionary-like syntax. + If the AsyncCurationSet instance doesn't exist, it creates a new one. + + Args: + curation_set_name (str): The name of the curation set. + + Returns: + AsyncCurationSet: The AsyncCurationSet instance for the specified name. + + Example: + >>> curation_sets = AsyncCurationSets(async_api_call) + >>> products_set = curation_sets["products"] + """ + from typesense.async_curation_set import AsyncCurationSet as PerSet + + return PerSet(self.api_call, curation_set_name) diff --git a/tests/curation_set_test.py b/tests/curation_set_test.py index 0d60ba2..bab741e 100644 --- a/tests/curation_set_test.py +++ b/tests/curation_set_test.py @@ -5,6 +5,8 @@ import pytest from tests.utils.version import is_v30_or_above +from typesense.async_curation_set import AsyncCurationSet +from typesense.async_curation_sets import AsyncCurationSets from typesense.client import Client from typesense.curation_set import CurationSet from typesense.curation_sets import CurationSets @@ -28,6 +30,11 @@ def test_paths(fake_curation_set: CurationSet) -> None: assert fake_curation_set._items_path == "/curation_sets/products/items" # noqa: WPS437 +def test_paths_async(fake_async_curation_set: AsyncCurationSet) -> None: + assert fake_async_curation_set._endpoint_path == "/curation_sets/products" # noqa: WPS437 + assert fake_async_curation_set._items_path == "/curation_sets/products/items" # noqa: WPS437 + + def test_actual_retrieve( actual_curation_sets: CurationSets, delete_all_curation_sets: None, @@ -182,3 +189,160 @@ def test_actual_delete_item( response = actual_curation_sets["products"].delete_item("rule-1") assert response == {"id": "rule-1"} + + +async def test_actual_retrieve_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSet object can retrieve a curation set from Typesense Server.""" + response = await actual_async_curation_sets["products"].retrieve() + + assert response == { + "items": [ + { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + }, + "stop_processing": True, + }, + ], + "name": "products", + } + + +async def test_actual_delete_async( + actual_async_curation_sets: AsyncCurationSets, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSet object can delete a curation set from Typesense Server.""" + response = await actual_async_curation_sets["products"].delete() + + assert response == {"name": "products"} + + +async def test_actual_list_items_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSet object can list items from Typesense Server.""" + response = await actual_async_curation_sets["products"].list_items() + + assert response == [ + { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + }, + "stop_processing": True, + }, + ] + + +async def test_actual_get_item_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSet object can get a specific item from Typesense Server.""" + response = await actual_async_curation_sets["products"].get_item("rule-1") + + assert response == { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + }, + "stop_processing": True, + } + + +async def test_actual_upsert_item_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSet object can upsert an item in Typesense Server.""" + payload: CurationItemSchema = { + "id": "rule-2", + "rule": {"query": "boot", "match": "exact"}, + "includes": [{"id": "456", "position": 2}], + "excludes": [{"id": "888"}], + } + response = await actual_async_curation_sets["products"].upsert_item( + "rule-2", payload + ) + + assert response == { + "excludes": [ + { + "id": "888", + }, + ], + "id": "rule-2", + "includes": [ + { + "id": "456", + "position": 2, + }, + ], + "rule": { + "match": "exact", + "query": "boot", + }, + } + + +async def test_actual_delete_item_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSet object can delete an item from Typesense Server.""" + response = await actual_async_curation_sets["products"].delete_item("rule-1") + + assert response == {"id": "rule-1"} diff --git a/tests/curation_sets_test.py b/tests/curation_sets_test.py index 3168bf3..02e32cb 100644 --- a/tests/curation_sets_test.py +++ b/tests/curation_sets_test.py @@ -1,7 +1,5 @@ """Tests for the CurationSets class.""" -from __future__ import annotations - import pytest from tests.utils.object_assertions import ( @@ -11,6 +9,8 @@ ) from tests.utils.version import is_v30_or_above from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_curation_sets import AsyncCurationSets from typesense.client import Client from typesense.curation_sets import CurationSets @@ -38,6 +38,17 @@ def test_init(fake_api_call: ApiCall) -> None: ) +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncCurationSets object is initialized correctly.""" + cur_sets = AsyncCurationSets(fake_async_api_call) + + assert_match_object(cur_sets.api_call, fake_async_api_call) + assert_object_lists_match( + cur_sets.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + + def test_actual_upsert( actual_curation_sets: CurationSets, delete_all_curation_sets: None, @@ -99,3 +110,66 @@ def test_actual_retrieve( "name": "products", }, ) + + +async def test_actual_upsert_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, +) -> None: + """Test that the AsyncCurationSets object can upsert a curation set on Typesense Server.""" + response = await actual_async_curation_sets["products"].upsert( + { + "items": [ + { + "id": "rule-1", + "rule": {"query": "shoe", "match": "contains"}, + "includes": [{"id": "123", "position": 1}], + "excludes": [{"id": "999"}], + } + ] + }, + ) + + assert response == { + "items": [ + { + "excludes": [ + { + "id": "999", + }, + ], + "filter_curated_hits": False, + "id": "rule-1", + "includes": [ + { + "id": "123", + "position": 1, + }, + ], + "remove_matched_tokens": False, + "rule": { + "match": "contains", + "query": "shoe", + }, + "stop_processing": True, + }, + ], + "name": "products", + } + + +async def test_actual_retrieve_async( + actual_async_curation_sets: AsyncCurationSets, + delete_all_curation_sets: None, + create_curation_set: None, +) -> None: + """Test that the AsyncCurationSets object can retrieve curation sets from Typesense Server.""" + response = await actual_async_curation_sets.retrieve() + + assert isinstance(response, list) + assert_to_contain_object( + response[0], + { + "name": "products", + }, + ) diff --git a/tests/fixtures/curation_set_fixtures.py b/tests/fixtures/curation_set_fixtures.py index 3fc61b5..c668ae5 100644 --- a/tests/fixtures/curation_set_fixtures.py +++ b/tests/fixtures/curation_set_fixtures.py @@ -4,6 +4,9 @@ import requests from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_curation_set import AsyncCurationSet +from typesense.async_curation_sets import AsyncCurationSets from typesense.curation_set import CurationSet from typesense.curation_sets import CurationSets @@ -69,3 +72,27 @@ def fake_curation_sets_fixture(fake_api_call: ApiCall) -> CurationSets: def fake_curation_set_fixture(fake_api_call: ApiCall) -> CurationSet: """Return a CurationSet object with test values.""" return CurationSet(fake_api_call, "products") + + +@pytest.fixture(scope="function", name="actual_async_curation_sets") +def actual_async_curation_sets_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncCurationSets: + """Return a AsyncCurationSets object using a real API.""" + return AsyncCurationSets(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_curation_sets") +def fake_async_curation_sets_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncCurationSets: + """Return a AsyncCurationSets object with test values.""" + return AsyncCurationSets(fake_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_curation_set") +def fake_async_curation_set_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncCurationSet: + """Return a AsyncCurationSet object with test values.""" + return AsyncCurationSet(fake_async_api_call, "products") From 1b704d0e9e06d72d23345bab04c76ddf42b5e5d6 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:49:28 +0200 Subject: [PATCH 17/32] feat(debug): add async support for debug operations - add AsyncDebug class for async debug information retrieval - add async tests for debug functionality - add async fixtures for testing async debug operations - remove future annotations imports from test files --- src/typesense/async_debug.py | 71 ++++++++++++++++++++++++++++++++ tests/debug_test.py | 30 +++++++++++++- tests/fixtures/debug_fixtures.py | 14 +++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/typesense/async_debug.py diff --git a/src/typesense/async_debug.py b/src/typesense/async_debug.py new file mode 100644 index 0000000..d790c82 --- /dev/null +++ b/src/typesense/async_debug.py @@ -0,0 +1,71 @@ +""" +This module provides async functionality for accessing debug information in Typesense. + +It contains the AsyncDebug class, which allows for retrieving debug information +asynchronously. + +Classes: + AsyncDebug: Manages async operations for accessing debug information in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.types.debug: Provides DebugResponseSchema type. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.types.debug import DebugResponseSchema + + +class AsyncDebug: + """ + Manages async operations for accessing debug information in the Typesense API. + + This class provides async methods to retrieve debug information from the Typesense server, + which can be useful for system diagnostics and troubleshooting. + + Attributes: + resource_path (str): The API resource path for debug operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + resource_path: typing.Final[str] = "/debug" + + def __init__(self, api_call: AsyncApiCall) -> None: + """ + Initialize the AsyncDebug instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + + async def retrieve(self) -> DebugResponseSchema: + """ + Retrieve debug information from the Typesense server. + + This method sends an async GET request to the debug endpoint and returns + the server's debug information. + + Returns: + DebugResponseSchema: A schema containing the debug information. + + Example: + >>> debug = AsyncDebug(async_api_call) + >>> info = await debug.retrieve() + >>> print(info["version"]) + """ + response: DebugResponseSchema = await self.api_call.get( + AsyncDebug.resource_path, + as_json=True, + entity_type=DebugResponseSchema, + ) + return response diff --git a/tests/debug_test.py b/tests/debug_test.py index 5415f84..a3ec6c0 100644 --- a/tests/debug_test.py +++ b/tests/debug_test.py @@ -1,8 +1,9 @@ """Tests for the Debug class.""" -from __future__ import annotations from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_debug import AsyncDebug from typesense.debug import Debug @@ -24,6 +25,22 @@ def test_init(fake_api_call: ApiCall) -> None: assert debug.resource_path == "/debug" # noqa: WPS437 +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncDebug object is initialized correctly.""" + debug = AsyncDebug(fake_async_api_call) + + assert_match_object(debug.api_call, fake_async_api_call) + assert_object_lists_match( + debug.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + debug.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert debug.resource_path == "/debug" # noqa: WPS437 + + def test_actual_retrieve(actual_debug: Debug) -> None: """Test that the Debug object can retrieve a debug on Typesense server and verify response structure.""" response = actual_debug.retrieve() @@ -33,3 +50,14 @@ def test_actual_retrieve(actual_debug: Debug) -> None: assert isinstance(response["state"], int) assert isinstance(response["version"], str) + + +async def test_actual_retrieve_async(actual_async_debug: AsyncDebug) -> None: + """Test that the AsyncDebug object can retrieve a debug on Typesense server and verify response structure.""" + response = await actual_async_debug.retrieve() + + assert "state" in response + assert "version" in response + + assert isinstance(response["state"], int) + assert isinstance(response["version"], str) diff --git a/tests/fixtures/debug_fixtures.py b/tests/fixtures/debug_fixtures.py index 13c29f6..0fe5419 100644 --- a/tests/fixtures/debug_fixtures.py +++ b/tests/fixtures/debug_fixtures.py @@ -3,6 +3,8 @@ import pytest from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_debug import AsyncDebug from typesense.debug import Debug @@ -16,3 +18,15 @@ def actual_debug_fixture(actual_api_call: ApiCall) -> Debug: def fake_debug_fixture(fake_api_call: ApiCall) -> Debug: """Return a debug object with test values.""" return Debug(fake_api_call) + + +@pytest.fixture(scope="function", name="actual_async_debug") +def actual_async_debug_fixture(actual_async_api_call: AsyncApiCall) -> AsyncDebug: + """Return a AsyncDebug object using a real API.""" + return AsyncDebug(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_debug") +def fake_async_debug_fixture(fake_async_api_call: AsyncApiCall) -> AsyncDebug: + """Return a AsyncDebug object with test values.""" + return AsyncDebug(fake_async_api_call) From 9a62bc260dfc59da31187201ad6276984cd949f2 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:52:04 +0200 Subject: [PATCH 18/32] feat(documents): add async support for document operations - add AsyncDocument class for async individual document operations - add AsyncDocuments class for async documents collection operations - add async tests for document and documents functionality - add async fixtures for testing async document operations --- src/typesense/async_document.py | 150 +++++++++ src/typesense/async_documents.py | 453 ++++++++++++++++++++++++++++ tests/document_test.py | 74 +++++ tests/documents_test.py | 109 +++++++ tests/fixtures/document_fixtures.py | 25 ++ 5 files changed, 811 insertions(+) create mode 100644 src/typesense/async_document.py create mode 100644 src/typesense/async_documents.py diff --git a/src/typesense/async_document.py b/src/typesense/async_document.py new file mode 100644 index 0000000..1a4c46b --- /dev/null +++ b/src/typesense/async_document.py @@ -0,0 +1,150 @@ +""" +This module provides async functionality for managing individual documents in Typesense collections. + +It contains the AsyncDocument class, which allows for retrieving, updating, and deleting +documents asynchronously. + +Classes: + AsyncDocument: Manages async operations on a single document in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.types.document: Provides various document schema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +from typesense.async_api_call import AsyncApiCall +from typesense.types.document import ( + DeleteSingleDocumentParameters, + DirtyValuesParameters, + DocumentSchema, + RetrieveParameters, +) + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +TDoc = typing.TypeVar("TDoc", bound=DocumentSchema) + + +class AsyncDocument(typing.Generic[TDoc]): + """ + Manages async operations on a single document in the Typesense API. + + This class provides async methods to retrieve, update, and delete a document. + + Attributes: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + collection_name (str): The name of the collection. + document_id (str): The ID of the document. + """ + + def __init__( + self, + api_call: AsyncApiCall, + collection_name: str, + document_id: str, + ) -> None: + """ + Initialize the AsyncDocument instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + collection_name (str): The name of the collection. + document_id (str): The ID of the document. + """ + self.api_call = api_call + self.collection_name = collection_name + self.document_id = document_id + + async def retrieve( + self, + retrieve_parameters: typing.Union[RetrieveParameters, None] = None, + ) -> TDoc: + """ + Retrieve this specific document. + + Args: + retrieve_parameters (Union[RetrieveParameters, None], optional): + Parameters for retrieving the document. + + Returns: + TDoc: The retrieved document. + """ + response: TDoc = await self.api_call.get( + endpoint=self._endpoint_path, + entity_type=typing.Dict[str, str], + as_json=True, + params=retrieve_parameters, + ) + return response + + async def update( + self, + document: TDoc, + dirty_values_parameters: typing.Union[DirtyValuesParameters, None] = None, + ) -> TDoc: + """ + Update this specific document. + + Args: + document (TDoc): The updated document data. + dirty_values_parameters (Union[DirtyValuesParameters, None], optional): + Parameters for handling dirty values. + + Returns: + TDoc: The updated document. + """ + response = await self.api_call.patch( + self._endpoint_path, + body=document, + params=dirty_values_parameters, + entity_type=typing.Dict[str, str], + ) + return typing.cast(TDoc, response) + + async def delete( + self, + delete_parameters: typing.Union[DeleteSingleDocumentParameters, None] = None, + ) -> TDoc: + """ + Delete this specific document. + + Args: + delete_parameters (Union[DeleteSingleDocumentParameters, None], optional): + Parameters for deletion. + + Returns: + TDoc: The deleted document. + """ + response: TDoc = await self.api_call.delete( + self._endpoint_path, + entity_type=typing.Dict[str, str], + params=delete_parameters, + ) + return response + + @property + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific document. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_collections import AsyncCollections + from typesense.async_documents import AsyncDocuments + + return "/".join( + [ + AsyncCollections.resource_path, + self.collection_name, + AsyncDocuments.resource_path, + self.document_id, + ], + ) diff --git a/src/typesense/async_documents.py b/src/typesense/async_documents.py new file mode 100644 index 0000000..926d0b7 --- /dev/null +++ b/src/typesense/async_documents.py @@ -0,0 +1,453 @@ +""" +This module provides async functionality for managing documents in Typesense collections. + +It contains the AsyncDocuments class, which allows for creating, updating, importing, exporting, +searching, and deleting documents asynchronously. + +Classes: + AsyncDocuments: Manages async operations on documents in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.async_document: Provides the AsyncDocument class for individual document operations. + - typesense.types.document: Provides various document schema types. + - typesense.preprocess: Provides stringify_search_params for search parameter processing. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import json +import sys + +from typesense.async_api_call import AsyncApiCall +from typesense.async_document import AsyncDocument +from typesense.exceptions import TypesenseClientError +from typesense.logger import logger +from typesense.preprocess import stringify_search_params +from typesense.types.document import ( + DeleteQueryParameters, + DeleteResponse, + DirtyValuesParameters, + DocumentExportParameters, + DocumentImportParameters, + DocumentImportParametersReturnDoc, + DocumentImportParametersReturnDocAndId, + DocumentImportParametersReturnId, + DocumentSchema, + DocumentWriteParameters, + ImportResponse, + ImportResponseFail, + ImportResponseSuccess, + ImportResponseWithDoc, + ImportResponseWithDocAndId, + ImportResponseWithId, + SearchParameters, + SearchResponse, + UpdateByFilterParameters, + UpdateByFilterResponse, +) + +# mypy: disable-error-code="misc" + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +TDoc = typing.TypeVar("TDoc", bound=DocumentSchema) + +_ImportParameters = typing.Union[ + DocumentImportParameters, + None, +] + + +class AsyncDocuments(typing.Generic[TDoc]): + """ + Manages async operations on documents in the Typesense API. + + This class provides async methods to interact with documents, including + creating, updating, importing, exporting, searching, and deleting them. + + Attributes: + resource_path (str): The API resource path for document operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + collection_name (str): The name of the collection. + documents (Dict[str, AsyncDocument[TDoc]]): A dictionary of AsyncDocument instances. + """ + + resource_path: typing.Final[str] = "documents" + + def __init__(self, api_call: AsyncApiCall, collection_name: str) -> None: + """ + Initialize the AsyncDocuments instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + collection_name (str): The name of the collection. + """ + self.api_call = api_call + self.collection_name = collection_name + self.documents: typing.Dict[str, AsyncDocument[TDoc]] = {} + + def __getitem__(self, document_id: str) -> AsyncDocument[TDoc]: + """ + Get or create an AsyncDocument instance for a given document ID. + + Args: + document_id (str): The ID of the document. + + Returns: + AsyncDocument[TDoc]: The AsyncDocument instance for the specified document ID. + """ + if document_id not in self.documents: + self.documents[document_id] = AsyncDocument( + self.api_call, + self.collection_name, + document_id, + ) + + return self.documents[document_id] + + async def create( + self, + document: TDoc, + dirty_values_parameters: typing.Union[DirtyValuesParameters, None] = None, + ) -> TDoc: + """ + Create a new document in the collection. + + Args: + document (TDoc): The document to create. + dirty_values_parameters (Union[DirtyValuesParameters, None], optional): + Parameters for handling dirty values. + + Returns: + TDoc: The created document. + """ + dirty_values_parameters = dirty_values_parameters or {} + dirty_values_parameters["action"] = "create" + response: TDoc = await self.api_call.post( + self._endpoint_path(), + body=document, + params=dirty_values_parameters, + as_json=True, + entity_type=typing.Dict[str, str], + ) + return response + + async def create_many( + self, + documents: typing.List[TDoc], + dirty_values_parameters: typing.Union[DirtyValuesParameters, None] = None, + ) -> typing.List[typing.Union[ImportResponseSuccess, ImportResponseFail[TDoc]]]: + """ + Create multiple documents in the collection. + + Args: + documents (List[TDoc]): The list of documents to create. + dirty_values_parameters (Union[DirtyValuesParameters, None], optional): + Parameters for handling dirty values. + + Returns: + List[Union[ImportResponseSuccess, ImportResponseFail[TDoc]]]: + The list of import responses. + """ + logger.warn("`create_many` is deprecated: please use `import_`.") + return await self.import_(documents, dirty_values_parameters) + + async def upsert( + self, + document: TDoc, + dirty_values_parameters: typing.Union[DirtyValuesParameters, None] = None, + ) -> TDoc: + """ + Create or update a document in the collection. + + Args: + document (TDoc): The document to upsert. + dirty_values_parameters (Union[DirtyValuesParameters, None], optional): + Parameters for handling dirty values. + + Returns: + TDoc: The upserted document. + """ + dirty_values_parameters = dirty_values_parameters or {} + dirty_values_parameters["action"] = "upsert" + response: TDoc = await self.api_call.post( + self._endpoint_path(), + body=document, + params=dirty_values_parameters, + as_json=True, + entity_type=typing.Dict[str, str], + ) + return response + + async def update( + self, + document: TDoc, + dirty_values_parameters: typing.Union[UpdateByFilterParameters, None] = None, + ) -> UpdateByFilterResponse: + """ + Update a document in the collection. + + Args: + document (TDoc): The document to update. + dirty_values_parameters (Union[UpdateByFilterParameters, None], optional): + Parameters for handling dirty values and filtering. + + Returns: + UpdateByFilterResponse: The response containing information about the update. + """ + dirty_values_parameters = dirty_values_parameters or {} + dirty_values_parameters["action"] = "update" + response: UpdateByFilterResponse = await self.api_call.patch( + self._endpoint_path(), + body=document, + params=dirty_values_parameters, + entity_type=UpdateByFilterResponse, + ) + return response + + async def import_jsonl(self, documents_jsonl: str) -> str: + """ + Import documents from a JSONL string. + + Args: + documents_jsonl (str): The JSONL string containing documents to import. + + Returns: + str: The import response as a string. + """ + logger.warning("`import_jsonl` is deprecated: please use `import_`.") + return await self.import_(documents_jsonl) + + @typing.overload + async def import_( + self, + documents: typing.List[TDoc], + import_parameters: DocumentImportParametersReturnDocAndId, + batch_size: typing.Union[int, None] = None, + ) -> typing.List[ + typing.Union[ImportResponseWithDocAndId[TDoc], ImportResponseFail[TDoc]] + ]: ... + + @typing.overload + async def import_( + self, + documents: typing.List[TDoc], + import_parameters: DocumentImportParametersReturnId, + batch_size: typing.Union[int, None] = None, + ) -> typing.List[typing.Union[ImportResponseWithId, ImportResponseFail[TDoc]]]: ... + + @typing.overload + async def import_( + self, + documents: typing.List[TDoc], + import_parameters: typing.Union[DocumentWriteParameters, None] = None, + batch_size: typing.Union[int, None] = None, + ) -> typing.List[typing.Union[ImportResponseSuccess, ImportResponseFail[TDoc]]]: ... + + @typing.overload + async def import_( + self, + documents: typing.List[TDoc], + import_parameters: DocumentImportParametersReturnDoc, + batch_size: typing.Union[int, None] = None, + ) -> typing.List[ + typing.Union[ImportResponseWithDoc[TDoc], ImportResponseFail[TDoc]] + ]: ... + + @typing.overload + async def import_( + self, + documents: typing.List[TDoc], + import_parameters: _ImportParameters, + batch_size: typing.Union[int, None] = None, + ) -> typing.List[ImportResponse[TDoc]]: ... + + @typing.overload + async def import_( + self, + documents: typing.Union[bytes, str], + import_parameters: _ImportParameters = None, + batch_size: typing.Union[int, None] = None, + ) -> str: ... + + async def import_( + self, + documents: typing.Union[bytes, str, typing.List[TDoc]], + import_parameters: _ImportParameters = None, + batch_size: typing.Union[int, None] = None, + ) -> typing.Union[ImportResponse[TDoc], str]: + """ + Import documents into the collection. + + This method supports various input types and import parameters. + It can handle both individual documents and batches of documents. + + Args: + documents: The documents to import. + import_parameters: Parameters for the import operation. + batch_size: The size of each batch for batch imports. + + Returns: + The import response, which can be a list of responses or a string. + + Raises: + TypesenseClientError: If an empty list of documents is provided. + """ + if isinstance(documents, (str, bytes)): + return await self._import_raw(documents, import_parameters) + + if batch_size: + return await self._batch_import(documents, import_parameters, batch_size) + + return await self._bulk_import(documents, import_parameters) + + async def export( + self, + export_parameters: typing.Union[DocumentExportParameters, None] = None, + ) -> str: + """ + Export documents from the collection. + + Args: + export_parameters (Union[DocumentExportParameters, None], optional): + Parameters for the export operation. + + Returns: + str: The exported documents as a string. + """ + api_response: str = await self.api_call.get( + self._endpoint_path("export"), + params=export_parameters, + as_json=False, + entity_type=str, + ) + return api_response + + async def search(self, search_parameters: SearchParameters) -> SearchResponse[TDoc]: + """ + Search for documents in the collection. + + Args: + search_parameters (SearchParameters): The search parameters. + + Returns: + SearchResponse[TDoc]: The search response containing matching documents. + """ + stringified_search_params = stringify_search_params(search_parameters) + response: SearchResponse[TDoc] = await self.api_call.get( + self._endpoint_path("search"), + params=stringified_search_params, + entity_type=SearchResponse, + as_json=True, + ) + return response + + async def delete( + self, + delete_parameters: typing.Union[DeleteQueryParameters, None] = None, + ) -> DeleteResponse: + """ + Delete documents from the collection based on given parameters. + + Args: + delete_parameters (Union[DeleteQueryParameters, None], optional): + Parameters for deletion. + + Returns: + DeleteResponse: The response containing information about the deletion. + """ + response: DeleteResponse = await self.api_call.delete( + self._endpoint_path(), + params=delete_parameters, + entity_type=DeleteResponse, + ) + return response + + def _endpoint_path(self, action: typing.Union[str, None] = None) -> str: + """ + Construct the API endpoint path for document operations. + + Args: + action (Union[str, None], optional): The action to perform. Defaults to None. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_collections import AsyncCollections + + action = action or "" + return "/".join( + [ + AsyncCollections.resource_path, + self.collection_name, + self.resource_path, + action, + ], + ) + + async def _import_raw( + self, + documents: typing.Union[bytes, str], + import_parameters: _ImportParameters, + ) -> str: + """Import raw document data.""" + response: str = await self.api_call.post( + self._endpoint_path("import"), + body=documents, + params=import_parameters, + as_json=False, + entity_type=str, + ) + + return response + + async def _batch_import( + self, + documents: typing.List[TDoc], + import_parameters: _ImportParameters, + batch_size: int, + ) -> ImportResponse[TDoc]: + """Import documents in batches.""" + response_objs: ImportResponse[TDoc] = [] + for batch_index in range(0, len(documents), batch_size): + batch = documents[batch_index : batch_index + batch_size] + api_response = await self._bulk_import(batch, import_parameters) + response_objs.extend(api_response) + return response_objs + + async def _bulk_import( + self, + documents: typing.List[TDoc], + import_parameters: _ImportParameters, + ) -> ImportResponse[TDoc]: + """Import a list of documents in bulk.""" + document_strs = [json.dumps(doc) for doc in documents] + if not document_strs: + raise TypesenseClientError("Cannot import an empty list of documents.") + + docs_import = "\n".join(document_strs) + res = await self.api_call.post( + self._endpoint_path("import"), + body=docs_import, + params=import_parameters, + entity_type=str, + as_json=False, + ) + return self._parse_import_response(res) + + def _parse_import_response(self, response: str) -> ImportResponse[TDoc]: + """Parse the import response string into a list of response objects.""" + response_objs: typing.List[ImportResponse] = [] + for res_obj_str in response.split("\n"): + try: + res_obj_json = json.loads(res_obj_str) + except json.JSONDecodeError as decode_error: + raise TypesenseClientError( + f"Invalid response - {res_obj_str}", + ) from decode_error + response_objs.append(res_obj_json) + return response_objs diff --git a/tests/document_test.py b/tests/document_test.py index fcd40c3..fae9d5d 100644 --- a/tests/document_test.py +++ b/tests/document_test.py @@ -10,6 +10,9 @@ assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_document import AsyncDocument +from typesense.async_documents import AsyncDocuments from typesense.document import Document from typesense.documents import Documents from typesense.exceptions import ObjectNotFound @@ -35,6 +38,26 @@ def test_init(fake_api_call: ApiCall) -> None: ) +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncDocument object is initialized correctly.""" + document = AsyncDocument(fake_async_api_call, "companies", "0") + + assert document.document_id == "0" + assert document.collection_name == "companies" + assert_match_object(document.api_call, fake_async_api_call) + assert_object_lists_match( + document.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + document.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert ( + document._endpoint_path == "/collections/companies/documents/0" # noqa: WPS437 + ) + + def test_actual_update( actual_documents: Documents, delete_all: None, @@ -109,3 +132,54 @@ def test_actual_delete_non_existent_ignore_not_found( ) assert response == {"id": "1"} + + +async def test_actual_update_async( + actual_async_documents: AsyncDocuments, + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncDocument object can update an document on Typesense Server.""" + response = await actual_async_documents["0"].update( + {"company_name": "Company", "num_employees": 20}, + { + "action": "update", + }, + ) + + assert_to_contain_object( + response, + {"id": "0", "company_name": "Company", "num_employees": 20}, + ) + + +async def test_actual_retrieve_async( + actual_async_documents: AsyncDocuments, + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncDocument object can retrieve an document from Typesense Server.""" + response = await actual_async_documents["0"].retrieve() + + assert_to_contain_object( + response, + {"id": "0", "company_name": "Company", "num_employees": 10}, + ) + + +async def test_actual_delete_async( + actual_async_documents: AsyncDocuments, + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncDocument object can delete an document from Typesense Server.""" + response = await actual_async_documents["0"].delete() + + assert response == { + "id": "0", + "company_name": "Company", + "num_employees": 10, + } diff --git a/tests/documents_test.py b/tests/documents_test.py index e355869..4a6db86 100644 --- a/tests/documents_test.py +++ b/tests/documents_test.py @@ -19,6 +19,8 @@ assert_to_contain_keys, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_documents import AsyncDocuments from typesense.documents import Documents from typesense.exceptions import InvalidParameter, TypesenseClientError @@ -40,6 +42,23 @@ def test_init(fake_api_call: ApiCall) -> None: assert not documents.documents +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncDocuments object is initialized correctly.""" + documents = AsyncDocuments(fake_async_api_call, "companies") + + assert_match_object(documents.api_call, fake_async_api_call) + assert_object_lists_match( + documents.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + documents.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not documents.documents + + def test_get_missing_document(fake_documents: Documents) -> None: """Test that the Documents object can get a missing document.""" document = fake_documents["1"] @@ -57,6 +76,24 @@ def test_get_missing_document(fake_documents: Documents) -> None: ) +def test_get_missing_document_async(fake_async_documents: AsyncDocuments) -> None: + """Test that the AsyncDocuments object can get a missing document.""" + document = fake_async_documents["1"] + + assert_match_object(document.api_call, fake_async_documents.api_call) + assert_object_lists_match( + document.api_call.node_manager.nodes, + fake_async_documents.api_call.node_manager.nodes, + ) + assert_match_object( + document.api_call.config.nearest_node, + fake_async_documents.api_call.config.nearest_node, + ) + assert ( + document._endpoint_path == "/collections/companies/documents/1" # noqa: WPS437 + ) + + def test_get_existing_document(fake_documents: Documents) -> None: """Test that the Documents object can get an existing document.""" document = fake_documents["1"] @@ -469,3 +506,75 @@ def test_search_invalid_parameters( "invalid": Companies(company_name="", id="", num_employees=0), }, ) + + +async def test_upsert_async( + actual_async_documents: AsyncDocuments[Companies], + delete_all: None, + create_collection: None, +) -> None: + """Test that the AsyncDocuments object can upsert a document on Typesense server.""" + company: Companies = { + "company_name": "company", + "id": "0", + "num_employees": 10, + } + response = await actual_async_documents.upsert(company) + + assert response == company + + +async def test_export_async( + actual_async_documents: AsyncDocuments[Companies], + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncDocuments object can export a document from Typesense server.""" + response = await actual_async_documents.export() + assert response == '{"company_name":"Company","id":"0","num_employees":10}' + + +async def test_delete_async( + actual_async_documents: AsyncDocuments[Companies], + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncDocuments object can delete a document from Typesense server.""" + response = await actual_async_documents.delete({"filter_by": "company_name:Company"}) + assert response == {"num_deleted": 1} + + +async def test_search_async( + actual_async_documents: AsyncDocuments[Companies], + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncDocuments object can search for documents on Typesense server.""" + response = await actual_async_documents.search( + { + "q": "com", + "query_by": "company_name", + }, + ) + + assert_to_contain_keys( + response, + [ + "facet_counts", + "found", + "hits", + "page", + "out_of", + "request_params", + "search_time_ms", + "search_cutoff", + ], + ) + + assert_to_contain_keys( + response.get("hits")[0], + ["document", "highlights", "highlight", "text_match", "text_match_info"], + ) diff --git a/tests/fixtures/document_fixtures.py b/tests/fixtures/document_fixtures.py index 8e829c3..05cf647 100644 --- a/tests/fixtures/document_fixtures.py +++ b/tests/fixtures/document_fixtures.py @@ -13,6 +13,9 @@ import typing_extensions as typing from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_document import AsyncDocument +from typesense.async_documents import AsyncDocuments from typesense.document import Document from typesense.documents import Documents @@ -53,6 +56,28 @@ def fake_document_fixture(fake_api_call: ApiCall) -> Document: return Document(fake_api_call, "companies", "0") +@pytest.fixture(scope="function", name="actual_async_documents") +def actual_async_documents_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncDocuments: + """Return a AsyncDocuments object using a real API.""" + return AsyncDocuments(actual_async_api_call, "companies") + + +@pytest.fixture(scope="function", name="fake_async_documents") +def fake_async_documents_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncDocuments: + """Return a AsyncDocuments object with test values.""" + return AsyncDocuments(fake_async_api_call, "companies") + + +@pytest.fixture(scope="function", name="fake_async_document") +def fake_async_document_fixture(fake_async_api_call: AsyncApiCall) -> AsyncDocument: + """Return a AsyncDocument object with test values.""" + return AsyncDocument(fake_async_api_call, "companies", "0") + + class Companies(typing.TypedDict): """Company data type.""" From f5e27fd0dd994d9a12df85e098db03df871ba914 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:53:05 +0200 Subject: [PATCH 19/32] feat(api-keys): add async support for key operations - add AsyncKey class for async individual key operations - add AsyncKeys class for async keys collection operations - add async tests for key and keys functionality - add async fixtures for testing async key operations --- src/typesense/async_key.py | 80 ++++++++++++++++ src/typesense/async_keys.py | 170 +++++++++++++++++++++++++++++++++ tests/fixtures/key_fixtures.py | 21 ++++ tests/key_test.py | 52 ++++++++++ tests/keys_test.py | 126 ++++++++++++++++++++++++ 5 files changed, 449 insertions(+) create mode 100644 src/typesense/async_key.py create mode 100644 src/typesense/async_keys.py diff --git a/src/typesense/async_key.py b/src/typesense/async_key.py new file mode 100644 index 0000000..2459bcd --- /dev/null +++ b/src/typesense/async_key.py @@ -0,0 +1,80 @@ +""" +This module provides async functionality for managing individual API keys in Typesense. + +It contains the AsyncKey class, which allows for retrieving and deleting +API keys asynchronously. + +Classes: + AsyncKey: Manages async operations on a single API key in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.types.key: Provides ApiKeyDeleteSchema and ApiKeySchema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.types.key import ApiKeyDeleteSchema, ApiKeySchema + + +class AsyncKey: + """ + Manages async operations on a single API key in the Typesense API. + + This class provides async methods to retrieve and delete an API key. + + Attributes: + key_id (int): The ID of the API key. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + def __init__(self, api_call: AsyncApiCall, key_id: int) -> None: + """ + Initialize the AsyncKey instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + key_id (int): The ID of the API key. + """ + self.key_id = key_id + self.api_call = api_call + + async def retrieve(self) -> ApiKeySchema: + """ + Retrieve this specific API key. + + Returns: + ApiKeySchema: The schema containing the API key details. + """ + response: ApiKeySchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=ApiKeySchema, + ) + return response + + async def delete(self) -> ApiKeyDeleteSchema: + """ + Delete this specific API key. + + Returns: + ApiKeyDeleteSchema: The schema containing the deletion response. + """ + response: ApiKeyDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=ApiKeyDeleteSchema, + ) + return response + + @property + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific API key. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_keys import AsyncKeys + + return "/".join([AsyncKeys.resource_path, str(self.key_id)]) diff --git a/src/typesense/async_keys.py b/src/typesense/async_keys.py new file mode 100644 index 0000000..6b0d48e --- /dev/null +++ b/src/typesense/async_keys.py @@ -0,0 +1,170 @@ +""" +This module provides async functionality for managing API keys in Typesense. + +It contains the AsyncKeys class, which allows for creating, retrieving, and +generating scoped search keys asynchronously. + +Classes: + AsyncKeys: Manages API keys in the Typesense API (async). + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.async_key: Provides the AsyncKey class for individual API key operations. + - typesense.types.document: Provides GenerateScopedSearchKeyParams type. + - typesense.types.key: Provides various API key schema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import base64 +import hashlib +import hmac +import json +import sys + +from typesense.async_api_call import AsyncApiCall +from typesense.async_key import AsyncKey +from typesense.types.document import GenerateScopedSearchKeyParams +from typesense.types.key import ( + ApiKeyCreateResponseSchema, + ApiKeyCreateSchema, + ApiKeyRetrieveSchema, + ApiKeySchema, +) + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + + +class AsyncKeys: + """ + Manages API keys in the Typesense API (async). + + This class provides async methods to create, retrieve, and generate scoped search keys. + + Attributes: + resource_path (str): The API endpoint path for key operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + keys (Dict[int, AsyncKey]): A dictionary of AsyncKey instances, keyed by key ID. + """ + + resource_path: typing.Final[str] = "/keys" + + def __init__(self, api_call: AsyncApiCall) -> None: + """ + Initialize the AsyncKeys instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + self.keys: typing.Dict[int, AsyncKey] = {} + + def __getitem__(self, key_id: int) -> AsyncKey: + """ + Get or create an AsyncKey instance for a given key ID. + + This method allows accessing API keys using dictionary-like syntax. + If the AsyncKey instance doesn't exist, it creates a new one. + + Args: + key_id (int): The ID of the API key. + + Returns: + AsyncKey: The AsyncKey instance for the specified key ID. + + Example: + >>> keys = AsyncKeys(async_api_call) + >>> key = keys[1] + """ + if not self.keys.get(key_id): + self.keys[key_id] = AsyncKey(self.api_call, key_id) + return self.keys[key_id] + + async def create(self, schema: ApiKeyCreateSchema) -> ApiKeyCreateResponseSchema: + """ + Create a new API key. + + Args: + schema (ApiKeyCreateSchema): The schema for creating the API key. + + Returns: + ApiKeyCreateResponseSchema: The created API key. + + Example: + >>> keys = AsyncKeys(async_api_call) + >>> key = await keys.create( + ... { + ... "actions": ["documents:search"], + ... "collections": ["companies"], + ... "description": "Search-only key", + ... } + ... ) + """ + response: ApiKeySchema = await self.api_call.post( + AsyncKeys.resource_path, + as_json=True, + body=schema, + entity_type=ApiKeySchema, + ) + return response + + def generate_scoped_search_key( + self, + search_key: str, + key_parameters: GenerateScopedSearchKeyParams, + ) -> bytes: + """ + Generate a scoped search key. + + Note: This is a synchronous method as it performs local computation + and does not make any API calls. Only a key generated with the + `documents:search` action will be accepted by the server. + + Args: + search_key (str): The search key to use as a base. + key_parameters (GenerateScopedSearchKeyParams): Parameters for the scoped key. + + Returns: + bytes: The generated scoped search key. + + Example: + >>> keys = AsyncKeys(async_api_call) + >>> scoped_key = keys.generate_scoped_search_key( + ... "KmacipDKNqAM3YiigXfw5pZvNOrPQUba", + ... {"q": "search query", "collection": "companies"}, + ... ) + """ + params_str = json.dumps(key_parameters) + digest = base64.b64encode( + hmac.new( + search_key.encode("utf-8"), + params_str.encode("utf-8"), + digestmod=hashlib.sha256, + ).digest(), + ) + key_prefix = search_key[:4] + raw_scoped_key = f"{digest.decode('utf-8')}{key_prefix}{params_str}" + return base64.b64encode(raw_scoped_key.encode("utf-8")) + + async def retrieve(self) -> ApiKeyRetrieveSchema: + """ + Retrieve all API keys. + + Returns: + ApiKeyRetrieveSchema: The schema containing all API keys. + + Example: + >>> keys = AsyncKeys(async_api_call) + >>> all_keys = await keys.retrieve() + >>> for key in all_keys["keys"]: + ... print(key["id"]) + """ + response: ApiKeyRetrieveSchema = await self.api_call.get( + AsyncKeys.resource_path, + entity_type=ApiKeyRetrieveSchema, + as_json=True, + ) + return response diff --git a/tests/fixtures/key_fixtures.py b/tests/fixtures/key_fixtures.py index 57833a8..d79a633 100644 --- a/tests/fixtures/key_fixtures.py +++ b/tests/fixtures/key_fixtures.py @@ -4,6 +4,9 @@ import requests from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_key import AsyncKey +from typesense.async_keys import AsyncKeys from typesense.key import Key from typesense.keys import Keys @@ -61,3 +64,21 @@ def fake_keys_fixture(fake_api_call: ApiCall) -> Keys: def fake_key_fixture(fake_api_call: ApiCall) -> Key: """Return a Key object with test values.""" return Key(fake_api_call, 1) + + +@pytest.fixture(scope="function", name="actual_async_keys") +def actual_async_keys_fixture(actual_async_api_call: AsyncApiCall) -> AsyncKeys: + """Return a AsyncKeys object using a real API.""" + return AsyncKeys(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_keys") +def fake_async_keys_fixture(fake_async_api_call: AsyncApiCall) -> AsyncKeys: + """Return a AsyncKeys object with test values.""" + return AsyncKeys(fake_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_key") +def fake_async_key_fixture(fake_async_api_call: AsyncApiCall) -> AsyncKey: + """Return a AsyncKey object with test values.""" + return AsyncKey(fake_async_api_call, 1) diff --git a/tests/key_test.py b/tests/key_test.py index d001dd9..9575e44 100644 --- a/tests/key_test.py +++ b/tests/key_test.py @@ -9,6 +9,9 @@ assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_key import AsyncKey +from typesense.async_keys import AsyncKeys from typesense.key import Key from typesense.keys import Keys @@ -30,6 +33,23 @@ def test_init(fake_api_call: ApiCall) -> None: assert key._endpoint_path == "/keys/3" # noqa: WPS437 +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncKey object is initialized correctly.""" + key = AsyncKey(fake_async_api_call, 3) + + assert key.key_id == 3 + assert_match_object(key.api_call, fake_async_api_call) + assert_object_lists_match( + key.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + key.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert key._endpoint_path == "/keys/3" # noqa: WPS437 + + def test_actual_retrieve( actual_keys: Keys, delete_all_keys: None, @@ -60,3 +80,35 @@ def test_actual_delete( response = actual_keys[create_key_id].delete() assert response == {"id": create_key_id} + + +async def test_actual_retrieve_async( + actual_async_keys: AsyncKeys, + delete_all_keys: None, + delete_all: None, + create_key_id: int, +) -> None: + """Test that the AsyncKey object can retrieve an key from Typesense Server.""" + response = await actual_async_keys[create_key_id].retrieve() + + assert_to_contain_object( + response, + { + "actions": ["documents:search"], + "collections": ["companies"], + "description": "Search-only key", + "id": create_key_id, + }, + ) + + +async def test_actual_delete_async( + actual_async_keys: AsyncKeys, + delete_all_keys: None, + delete_all: None, + create_key_id: int, +) -> None: + """Test that the AsyncKey object can delete an key from Typesense Server.""" + response = await actual_async_keys[create_key_id].delete() + + assert response == {"id": create_key_id} diff --git a/tests/keys_test.py b/tests/keys_test.py index 2da40b5..4786879 100644 --- a/tests/keys_test.py +++ b/tests/keys_test.py @@ -14,6 +14,8 @@ assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_keys import AsyncKeys from typesense.keys import Keys @@ -34,6 +36,23 @@ def test_init(fake_api_call: ApiCall) -> None: assert not keys.keys +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncKeys object is initialized correctly.""" + keys = AsyncKeys(fake_async_api_call) + + assert_match_object(keys.api_call, fake_async_api_call) + assert_object_lists_match( + keys.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + keys.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not keys.keys + + def test_get_missing_key(fake_keys: Keys) -> None: """Test that the Keys object can get a missing key.""" key = fake_keys[1] @@ -49,6 +68,22 @@ def test_get_missing_key(fake_keys: Keys) -> None: assert key._endpoint_path == "/keys/1" # noqa: WPS437 +def test_get_missing_key_async(fake_async_keys: AsyncKeys) -> None: + """Test that the AsyncKeys object can get a missing key.""" + key = fake_async_keys[1] + + assert_match_object(key.api_call, fake_async_keys.api_call) + assert_object_lists_match( + key.api_call.node_manager.nodes, + fake_async_keys.api_call.node_manager.nodes, + ) + assert_match_object( + key.api_call.config.nearest_node, + fake_async_keys.api_call.config.nearest_node, + ) + assert key._endpoint_path == "/keys/1" # noqa: WPS437 + + def test_get_existing_key(fake_keys: Keys) -> None: """Test that the Keys object can get an existing key.""" key = fake_keys[1] @@ -59,6 +94,16 @@ def test_get_existing_key(fake_keys: Keys) -> None: assert key is fetched_key +def test_get_existing_key_async(fake_async_keys: AsyncKeys) -> None: + """Test that the AsyncKeys object can get an existing key.""" + key = fake_async_keys[1] + fetched_key = fake_async_keys[1] + + assert len(fake_async_keys.keys) == 1 + + assert key is fetched_key + + def test_actual_create( actual_keys: Keys, ) -> None: @@ -138,3 +183,84 @@ def test_generate_scoped_search_key( ).decode("utf-8") assert extracted_key["digest"] == recomputed_digest + + +async def test_actual_create_async( + actual_async_keys: AsyncKeys, +) -> None: + """Test that the AsyncKeys object can create an key on Typesense Server.""" + response = await actual_async_keys.create( + { + "actions": ["documents:search"], + "collections": ["companies"], + "description": "Search-only key", + }, + ) + + assert_to_contain_object( + response, + { + "actions": ["documents:search"], + "collections": ["companies"], + "description": "Search-only key", + "autodelete": False, + }, + ) + + +async def test_actual_retrieve_async( + actual_async_keys: AsyncKeys, + delete_all: None, + delete_all_keys: None, + create_key_id: int, +) -> None: + """Test that the AsyncKeys object can retrieve an key from Typesense Server.""" + response = await actual_async_keys.retrieve() + assert len(response["keys"]) == 1 + assert_to_contain_object( + response["keys"][0], + { + "actions": ["documents:search"], + "collections": ["companies"], + "description": "Search-only key", + "autodelete": False, + "id": create_key_id, + }, + ) + + +def test_generate_scoped_search_key_async( + fake_async_keys: AsyncKeys, +) -> None: + """Test that the AsyncKeys object can generate a scoped search key.""" + # Use a real key that works on Typesense server + search_key = "KmacipDKNqAM3YiigXfw5pZvNOrPQUba" + search_parameters = { + "q": "search query", + "collection": "companies", + "filter_by": "num_employees:>10", + } + + key = fake_async_keys.generate_scoped_search_key(search_key, search_parameters) + + decoded_key = base64.b64decode(key).decode("utf-8") + + extracted_key = { + "digest": decoded_key[:44], + "key_prefix": decoded_key[44:48], + "params_str": decoded_key[48:], + } + assert extracted_key["key_prefix"] == search_key[:4] + + expected_params_str = json.dumps(search_parameters) + assert extracted_key["params_str"] == expected_params_str + + recomputed_digest = base64.b64encode( + hmac.new( + search_key.encode("utf-8"), + expected_params_str.encode("utf-8"), + digestmod=hashlib.sha256, + ).digest(), + ).decode("utf-8") + + assert extracted_key["digest"] == recomputed_digest From c311ab55364af0e1c555258448ee750a2be674f2 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:53:29 +0200 Subject: [PATCH 20/32] feat(metrics): add async support for metrics operations - add AsyncMetrics class for async metrics retrieval - add async tests for metrics functionality - add async fixtures for testing async metrics operations --- src/typesense/async_metrics.py | 69 ++++++++++++++++++++++++++++++ tests/fixtures/metrics_fixtures.py | 14 ++++++ tests/metrics_test.py | 64 ++++++++++++++++++++++++++- 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/typesense/async_metrics.py diff --git a/src/typesense/async_metrics.py b/src/typesense/async_metrics.py new file mode 100644 index 0000000..30957fe --- /dev/null +++ b/src/typesense/async_metrics.py @@ -0,0 +1,69 @@ +""" +This module provides async functionality for retrieving metrics from the Typesense API. + +It contains the AsyncMetrics class, which handles async API operations for retrieving +system and Typesense metrics such as CPU, memory, disk, and network usage. + +Classes: + MetricsResponse: Type definition for metrics response (imported from typesense.metrics). + AsyncMetrics: Manages async retrieval of metrics from the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.metrics: Provides MetricsResponse type definitions. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.metrics import MetricsResponse + + +class AsyncMetrics: + """ + Manages async metrics retrieval from the Typesense API. + + This class provides async methods to retrieve system and Typesense metrics + such as CPU, memory, disk, and network usage. + + Attributes: + resource_path (str): The base path for metrics endpoint. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + resource_path: typing.Final[str] = "/metrics.json" + + def __init__(self, api_call: AsyncApiCall): + """ + Initialize the AsyncMetrics instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + + async def retrieve(self) -> MetricsResponse: + """ + Retrieve metrics from the Typesense API. + + Returns: + MetricsResponse: A dictionary containing system and Typesense metrics. + + Example: + >>> metrics = AsyncMetrics(async_api_call) + >>> response = await metrics.retrieve() + >>> print(response["system_cpu_active_percentage"]) + """ + response: MetricsResponse = await self.api_call.get( + AsyncMetrics.resource_path, + as_json=True, + entity_type=MetricsResponse, + ) + return response diff --git a/tests/fixtures/metrics_fixtures.py b/tests/fixtures/metrics_fixtures.py index 7da5bc2..3a7be37 100644 --- a/tests/fixtures/metrics_fixtures.py +++ b/tests/fixtures/metrics_fixtures.py @@ -3,6 +3,8 @@ import pytest from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_metrics import AsyncMetrics from typesense.metrics import Metrics @@ -10,3 +12,15 @@ def actual_debug_fixture(actual_api_call: ApiCall) -> Metrics: """Return a Debug object using a real API.""" return Metrics(actual_api_call) + + +@pytest.fixture(scope="function", name="actual_async_metrics") +def actual_async_metrics_fixture(actual_async_api_call: AsyncApiCall) -> AsyncMetrics: + """Return a AsyncMetrics object using a real API.""" + return AsyncMetrics(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_metrics") +def fake_async_metrics_fixture(fake_async_api_call: AsyncApiCall) -> AsyncMetrics: + """Return a AsyncMetrics object with test values.""" + return AsyncMetrics(fake_async_api_call) diff --git a/tests/metrics_test.py b/tests/metrics_test.py index 01bb9fa..8f63043 100644 --- a/tests/metrics_test.py +++ b/tests/metrics_test.py @@ -1,12 +1,51 @@ -"""Tests for the Debug class.""" +"""Tests for the Metrics class.""" from __future__ import annotations +from tests.utils.object_assertions import ( + assert_match_object, + assert_object_lists_match, +) +from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_metrics import AsyncMetrics from typesense.metrics import Metrics +def test_init(fake_api_call: ApiCall) -> None: + """Test that the Metrics object is initialized correctly.""" + metrics = Metrics(fake_api_call) + + assert_match_object(metrics.api_call, fake_api_call) + assert_object_lists_match( + metrics.api_call.node_manager.nodes, + fake_api_call.node_manager.nodes, + ) + assert_match_object( + metrics.api_call.config.nearest_node, + fake_api_call.config.nearest_node, + ) + assert metrics.resource_path == "/metrics.json" # noqa: WPS437 + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncMetrics object is initialized correctly.""" + metrics = AsyncMetrics(fake_async_api_call) + + assert_match_object(metrics.api_call, fake_async_api_call) + assert_object_lists_match( + metrics.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + metrics.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert metrics.resource_path == "/metrics.json" # noqa: WPS437 + + def test_actual_retrieve(actual_metrics: Metrics) -> None: - """Test that the Debug object can retrieve a debug on Typesense server and verify response structure.""" + """Test that the Metrics object can retrieve metrics on Typesense server and verify response structure.""" response = actual_metrics.retrieve() assert "system_cpu_active_percentage" in response @@ -24,3 +63,24 @@ def test_actual_retrieve(actual_metrics: Metrics) -> None: assert "typesense_memory_metadata_bytes" in response assert "typesense_memory_resident_bytes" in response assert "typesense_memory_retained_bytes" in response + + +async def test_actual_retrieve_async(actual_async_metrics: AsyncMetrics) -> None: + """Test that the AsyncMetrics object can retrieve metrics on Typesense server and verify response structure.""" + response = await actual_async_metrics.retrieve() + + assert "system_cpu_active_percentage" in response + assert "system_disk_total_bytes" in response + assert "system_disk_used_bytes" in response + assert "system_memory_total_bytes" in response + assert "system_memory_used_bytes" in response + assert "system_network_received_bytes" in response + assert "system_network_sent_bytes" in response + assert "typesense_memory_active_bytes" in response + assert "typesense_memory_allocated_bytes" in response + assert "typesense_memory_fragmentation_ratio" in response + + assert "typesense_memory_mapped_bytes" in response + assert "typesense_memory_metadata_bytes" in response + assert "typesense_memory_resident_bytes" in response + assert "typesense_memory_retained_bytes" in response From 9956fd9896c6027a74c392d277d6af57bac5234d Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 18:53:56 +0200 Subject: [PATCH 21/32] feat(multi-search): add async support for multi-search operations - add AsyncMultiSearch class for async multi-search operations - add async tests for multi-search functionality - add async fixtures for testing async multi-search operations --- src/typesense/async_multi_search.py | 108 ++++++++++++++ tests/fixtures/multi_search_fixtures.py | 18 +++ tests/multi_search_test.py | 181 +++++++++++++++++++++++- 3 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 src/typesense/async_multi_search.py diff --git a/src/typesense/async_multi_search.py b/src/typesense/async_multi_search.py new file mode 100644 index 0000000..c95e79c --- /dev/null +++ b/src/typesense/async_multi_search.py @@ -0,0 +1,108 @@ +""" +This module provides async functionality for performing multi-search operations in the Typesense API. + +It contains the AsyncMultiSearch class, which allows for executing multiple search queries +asynchronously in a single API call. + +Classes: + AsyncMultiSearch: Manages async multi-search operations in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.preprocess: Provides the stringify_search_params function for parameter processing. + - typesense.types.document: Provides the MultiSearchCommonParameters type. + - typesense.types.multi_search: Provides MultiSearchRequestSchema and MultiSearchResponse types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +from typesense.async_api_call import AsyncApiCall +from typesense.preprocess import stringify_search_params +from typesense.types.document import MultiSearchCommonParameters +from typesense.types.multi_search import MultiSearchRequestSchema, MultiSearchResponse + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + + +class AsyncMultiSearch: + """ + Manages async multi-search operations in the Typesense API. + + This class provides async methods to perform multiple search queries in a single API call. + + Attributes: + resource_path (str): The API endpoint path for multi-search operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + resource_path: typing.Final[str] = "/multi_search" + + def __init__(self, api_call: AsyncApiCall) -> None: + """ + Initialize the AsyncMultiSearch instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + + async def perform( + self, + search_queries: MultiSearchRequestSchema, + common_params: typing.Union[MultiSearchCommonParameters, None] = None, + ) -> MultiSearchResponse: + """ + Perform a multi-search operation. + + This method allows executing multiple search queries in a single API call. + It processes the search parameters, sends the request to the Typesense API, + and returns the multi-search response. + + Args: + search_queries (MultiSearchRequestSchema): + A dictionary containing the list of search queries to perform. + The dictionary should have a 'searches' key with a list of search + parameter dictionaries. + common_params (Union[MultiSearchCommonParameters, None], optional): + Common parameters to apply to all search queries. Defaults to None. + + Returns: + MultiSearchResponse: + The response from the multi-search operation, containing + the results of all search queries. + + Example: + >>> multi_search = AsyncMultiSearch(async_api_call) + >>> response = await multi_search.perform( + ... { + ... "searches": [ + ... { + ... "q": "com", + ... "query_by": "company_name", + ... "collection": "companies", + ... }, + ... ], + ... } + ... ) + """ + stringified_search_params = [ + stringify_search_params(search_params) + for search_params in search_queries.get("searches") + ] + search_body = { + "searches": stringified_search_params, + "union": search_queries.get("union", False), + } + response: MultiSearchResponse = await self.api_call.post( + AsyncMultiSearch.resource_path, + body=search_body, + params=common_params, + as_json=True, + entity_type=MultiSearchResponse, + ) + return response diff --git a/tests/fixtures/multi_search_fixtures.py b/tests/fixtures/multi_search_fixtures.py index 171ebac..88ca996 100644 --- a/tests/fixtures/multi_search_fixtures.py +++ b/tests/fixtures/multi_search_fixtures.py @@ -3,6 +3,8 @@ import pytest from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_multi_search import AsyncMultiSearch from typesense.multi_search import MultiSearch @@ -10,3 +12,19 @@ def actual_multi_search_fixture(actual_api_call: ApiCall) -> MultiSearch: """Return a MultiSearch object using a real API.""" return MultiSearch(actual_api_call) + + +@pytest.fixture(scope="function", name="actual_async_multi_search") +def actual_async_multi_search_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncMultiSearch: + """Return a AsyncMultiSearch object using a real API.""" + return AsyncMultiSearch(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_multi_search") +def fake_async_multi_search_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncMultiSearch: + """Return a AsyncMultiSearch object with test values.""" + return AsyncMultiSearch(fake_async_api_call) diff --git a/tests/multi_search_test.py b/tests/multi_search_test.py index eac190b..fdd6462 100644 --- a/tests/multi_search_test.py +++ b/tests/multi_search_test.py @@ -10,25 +10,42 @@ ) from typesense import exceptions from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_multi_search import AsyncMultiSearch from typesense.multi_search import MultiSearch from typesense.types.multi_search import MultiSearchRequestSchema def test_init(fake_api_call: ApiCall) -> None: - """Test that the Document object is initialized correctly.""" - documents = MultiSearch(fake_api_call) + """Test that the MultiSearch object is initialized correctly.""" + multi_search = MultiSearch(fake_api_call) - assert_match_object(documents.api_call, fake_api_call) + assert_match_object(multi_search.api_call, fake_api_call) assert_object_lists_match( - documents.api_call.node_manager.nodes, + multi_search.api_call.node_manager.nodes, fake_api_call.node_manager.nodes, ) assert_match_object( - documents.api_call.config.nearest_node, + multi_search.api_call.config.nearest_node, fake_api_call.config.nearest_node, ) +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncMultiSearch object is initialized correctly.""" + multi_search = AsyncMultiSearch(fake_async_api_call) + + assert_match_object(multi_search.api_call, fake_async_api_call) + assert_object_lists_match( + multi_search.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + multi_search.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + def test_multi_search_single_search( actual_multi_search: MultiSearch, actual_api_call: ApiCall, @@ -220,3 +237,157 @@ def test_search_invalid_parameters( ], }, ) + + +async def test_multi_search_single_search_async( + actual_async_multi_search: AsyncMultiSearch, + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncMultiSearch object can perform a single search.""" + request_params: MultiSearchRequestSchema = { + "searches": [ + {"q": "com", "query_by": "company_name", "collection": "companies"}, + ], + } + response = await actual_async_multi_search.perform( + search_queries=request_params, + ) + + assert len(response.get("results")) == 1 + assert_to_contain_keys( + response.get("results")[0], + [ + "facet_counts", + "found", + "hits", + "page", + "out_of", + "request_params", + "search_time_ms", + "search_cutoff", + ], + ) + + assert_to_contain_keys( + response.get("results")[0].get("hits")[0], + ["document", "highlights", "highlight", "text_match", "text_match_info"], + ) + + +async def test_multi_search_multiple_searches_async( + actual_async_multi_search: AsyncMultiSearch, + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncMultiSearch object can perform multiple searches.""" + request_params: MultiSearchRequestSchema = { + "searches": [ + {"q": "com", "query_by": "company_name", "collection": "companies"}, + {"q": "company", "query_by": "company_name", "collection": "companies"}, + ], + } + + response = await actual_async_multi_search.perform(search_queries=request_params) + + assert len(response.get("results")) == len(request_params.get("searches")) + for search_results in response.get("results"): + assert_to_contain_keys( + search_results, + [ + "facet_counts", + "found", + "hits", + "page", + "out_of", + "request_params", + "search_time_ms", + "search_cutoff", + ], + ) + + assert_to_contain_keys( + search_results.get("hits")[0], + ["document", "highlights", "highlight", "text_match", "text_match_info"], + ) + + +async def test_multi_search_union_async( + actual_async_multi_search: AsyncMultiSearch, + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncMultiSearch object can perform multiple searches with union.""" + request_params: MultiSearchRequestSchema = { + "union": True, + "searches": [ + {"q": "com", "query_by": "company_name", "collection": "companies"}, + {"q": "company", "query_by": "company_name", "collection": "companies"}, + ], + } + + response = await actual_async_multi_search.perform(search_queries=request_params) + + assert_to_contain_keys( + response, + [ + "found", + "hits", + "page", + "out_of", + "union_request_params", + "search_time_ms", + "search_cutoff", + ], + ) + + assert_to_contain_keys( + response.get("hits")[0], + [ + "collection", + "document", + "highlights", + "highlight", + "text_match", + "text_match_info", + "search_index", + ], + ) + + +async def test_multi_search_array_async( + actual_async_multi_search: AsyncMultiSearch, + delete_all: None, + create_collection: None, + create_document: None, +) -> None: + """Test that the AsyncMultiSearch object can perform a search with an array query_by.""" + request_params: MultiSearchRequestSchema = { + "searches": [ + {"q": "com", "query_by": ["company_name"], "collection": "companies"}, + ], + } + response = await actual_async_multi_search.perform(search_queries=request_params) + + assert len(response.get("results")) == 1 + assert_to_contain_keys( + response.get("results")[0], + [ + "facet_counts", + "found", + "hits", + "page", + "out_of", + "request_params", + "search_time_ms", + "search_cutoff", + ], + ) + + assert_to_contain_keys( + response.get("results")[0].get("hits")[0], + ["document", "highlights", "highlight", "text_match", "text_match_info"], + ) From 6a6e4d374b39df5ffcf3d9bdc599f4298cfcfc55 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:11:26 +0200 Subject: [PATCH 22/32] feat(nl-search): add async support for nl search model operations - add AsyncNLSearchModel class for async individual model operations - add AsyncNLSearchModels class for async mod --- src/typesense/async_nl_search_model.py | 102 ++++++++++++++++ src/typesense/async_nl_search_models.py | 130 +++++++++++++++++++++ tests/fixtures/nl_search_model_fixtures.py | 27 +++++ tests/nl_search_model_test.py | 85 ++++++++++++++ tests/nl_search_models_test.py | 96 +++++++++++++++ 5 files changed, 440 insertions(+) create mode 100644 src/typesense/async_nl_search_model.py create mode 100644 src/typesense/async_nl_search_models.py diff --git a/src/typesense/async_nl_search_model.py b/src/typesense/async_nl_search_model.py new file mode 100644 index 0000000..70bf4af --- /dev/null +++ b/src/typesense/async_nl_search_model.py @@ -0,0 +1,102 @@ +""" +This module provides async functionality for managing individual NL search models in Typesense. + +It contains the AsyncNLSearchModel class, which allows for retrieving, updating, and deleting +NL search models asynchronously. + +Classes: + AsyncNLSearchModel: Manages async operations on a single NL search model in the Typesense API. + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.types.nl_search_model: Provides NLSearchModelDeleteSchema, NLSearchModelSchema, and NLSearchModelUpdateSchema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.types.nl_search_model import ( + NLSearchModelDeleteSchema, + NLSearchModelSchema, + NLSearchModelUpdateSchema, +) + + +class AsyncNLSearchModel: + """ + Manages async operations on a single NL search model in the Typesense API. + + This class provides async methods to retrieve, update, and delete an NL search model. + + Attributes: + model_id (str): The ID of the NL search model. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + def __init__(self, api_call: AsyncApiCall, model_id: str) -> None: + """ + Initialize the AsyncNLSearchModel instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + model_id (str): The ID of the NL search model. + """ + self.model_id = model_id + self.api_call = api_call + + async def retrieve(self) -> NLSearchModelSchema: + """ + Retrieve this specific NL search model. + + Returns: + NLSearchModelSchema: The schema containing the NL search model details. + """ + response: NLSearchModelSchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=NLSearchModelSchema, + ) + return response + + async def update(self, model: NLSearchModelUpdateSchema) -> NLSearchModelSchema: + """ + Update this specific NL search model. + + Args: + model (NLSearchModelUpdateSchema): + The schema containing the updated model details. + + Returns: + NLSearchModelSchema: The schema containing the updated NL search model. + """ + response: NLSearchModelSchema = await self.api_call.put( + self._endpoint_path, + body=model, + entity_type=NLSearchModelSchema, + ) + return response + + async def delete(self) -> NLSearchModelDeleteSchema: + """ + Delete this specific NL search model. + + Returns: + NLSearchModelDeleteSchema: The schema containing the deletion response. + """ + response: NLSearchModelDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=NLSearchModelDeleteSchema, + ) + return response + + @property + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific NL search model. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_nl_search_models import AsyncNLSearchModels + + return "/".join([AsyncNLSearchModels.resource_path, self.model_id]) diff --git a/src/typesense/async_nl_search_models.py b/src/typesense/async_nl_search_models.py new file mode 100644 index 0000000..7e85937 --- /dev/null +++ b/src/typesense/async_nl_search_models.py @@ -0,0 +1,130 @@ +""" +This module provides async functionality for managing NL search models in Typesense. + +It contains the AsyncNLSearchModels class, which allows for creating, retrieving, and +accessing individual NL search models asynchronously. + +Classes: + AsyncNLSearchModels: Manages NL search models in the Typesense API (async). + +Dependencies: + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + - typesense.async_nl_search_model: Provides the AsyncNLSearchModel class for individual NL search model operations. + - typesense.types.nl_search_model: Provides NLSearchModelCreateSchema, NLSearchModelSchema, and NLSearchModelsRetrieveSchema types. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +from typesense.async_api_call import AsyncApiCall +from typesense.async_nl_search_model import AsyncNLSearchModel +from typesense.types.nl_search_model import ( + NLSearchModelCreateSchema, + NLSearchModelSchema, + NLSearchModelsRetrieveSchema, +) + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + + +class AsyncNLSearchModels: + """ + Manages NL search models in the Typesense API (async). + + This class provides async methods to create, retrieve, and access individual NL search models. + + Attributes: + resource_path (str): The API endpoint path for NL search models operations. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + nl_search_models (Dict[str, AsyncNLSearchModel]): + A dictionary of AsyncNLSearchModel instances, keyed by model ID. + """ + + resource_path: typing.Final[str] = "/nl_search_models" + + def __init__(self, api_call: AsyncApiCall) -> None: + """ + Initialize the AsyncNLSearchModels instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + self.nl_search_models: typing.Dict[str, AsyncNLSearchModel] = {} + + def __getitem__(self, model_id: str) -> AsyncNLSearchModel: + """ + Get or create an AsyncNLSearchModel instance for a given model ID. + + This method allows accessing NL search models using dictionary-like syntax. + If the AsyncNLSearchModel instance doesn't exist, it creates a new one. + + Args: + model_id (str): The ID of the NL search model. + + Returns: + AsyncNLSearchModel: The AsyncNLSearchModel instance for the specified model ID. + + Example: + >>> nl_search_models = AsyncNLSearchModels(async_api_call) + >>> model = nl_search_models["model_id"] + """ + if model_id not in self.nl_search_models: + self.nl_search_models[model_id] = AsyncNLSearchModel( + self.api_call, + model_id, + ) + return self.nl_search_models[model_id] + + async def create(self, model: NLSearchModelCreateSchema) -> NLSearchModelSchema: + """ + Create a new NL search model. + + Args: + model (NLSearchModelCreateSchema): + The schema for creating the NL search model. + + Returns: + NLSearchModelSchema: The created NL search model. + + Example: + >>> nl_search_models = AsyncNLSearchModels(async_api_call) + >>> model = await nl_search_models.create( + ... { + ... "api_key": "key", + ... "model_name": "openai/gpt-3.5-turbo", + ... "system_prompt": "System prompt", + ... } + ... ) + """ + response: NLSearchModelSchema = await self.api_call.post( + endpoint=AsyncNLSearchModels.resource_path, + entity_type=NLSearchModelSchema, + as_json=True, + body=model, + ) + return response + + async def retrieve(self) -> NLSearchModelsRetrieveSchema: + """ + Retrieve all NL search models. + + Returns: + NLSearchModelsRetrieveSchema: A list of all NL search models. + + Example: + >>> nl_search_models = AsyncNLSearchModels(async_api_call) + >>> all_models = await nl_search_models.retrieve() + >>> for model in all_models: + ... print(model["id"]) + """ + response: NLSearchModelsRetrieveSchema = await self.api_call.get( + endpoint=AsyncNLSearchModels.resource_path, + entity_type=NLSearchModelsRetrieveSchema, + as_json=True, + ) + return response diff --git a/tests/fixtures/nl_search_model_fixtures.py b/tests/fixtures/nl_search_model_fixtures.py index 4949b98..75094ff 100644 --- a/tests/fixtures/nl_search_model_fixtures.py +++ b/tests/fixtures/nl_search_model_fixtures.py @@ -7,6 +7,9 @@ from dotenv import load_dotenv from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_nl_search_model import AsyncNLSearchModel +from typesense.async_nl_search_models import AsyncNLSearchModels from typesense.nl_search_model import NLSearchModel from typesense.nl_search_models import NLSearchModels @@ -76,3 +79,27 @@ def actual_nl_search_models_fixture( ) -> NLSearchModels: """Return an NLSearchModels object using a real API.""" return NLSearchModels(actual_api_call) + + +@pytest.fixture(scope="function", name="actual_async_nl_search_models") +def actual_async_nl_search_models_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncNLSearchModels: + """Return a AsyncNLSearchModels object using a real API.""" + return AsyncNLSearchModels(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_nl_search_models") +def fake_async_nl_search_models_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncNLSearchModels: + """Return a AsyncNLSearchModels object with test values.""" + return AsyncNLSearchModels(fake_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_nl_search_model") +def fake_async_nl_search_model_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncNLSearchModel: + """Return a AsyncNLSearchModel object with test values.""" + return AsyncNLSearchModel(fake_async_api_call, "nl_search_model_id") diff --git a/tests/nl_search_model_test.py b/tests/nl_search_model_test.py index d47a536..2da60b8 100644 --- a/tests/nl_search_model_test.py +++ b/tests/nl_search_model_test.py @@ -11,6 +11,9 @@ assert_to_contain_keys, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_nl_search_model import AsyncNLSearchModel +from typesense.async_nl_search_models import AsyncNLSearchModels from typesense.nl_search_model import NLSearchModel from typesense.nl_search_models import NLSearchModels @@ -40,6 +43,29 @@ def test_init(fake_api_call: ApiCall) -> None: ) +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncNLSearchModel object is initialized correctly.""" + nl_search_model = AsyncNLSearchModel( + fake_async_api_call, + "nl_search_model_id", + ) + + assert nl_search_model.model_id == "nl_search_model_id" + assert_match_object(nl_search_model.api_call, fake_async_api_call) + assert_object_lists_match( + nl_search_model.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + nl_search_model.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert ( + nl_search_model._endpoint_path # noqa: WPS437 + == "/nl_search_models/nl_search_model_id" + ) + + @pytest.mark.open_ai def test_actual_retrieve( actual_nl_search_models: NLSearchModels, @@ -97,3 +123,62 @@ def test_actual_delete( ) assert response.get("id") == create_nl_search_model + + +@pytest.mark.open_ai +async def test_actual_retrieve_async( + actual_async_nl_search_models: AsyncNLSearchModels, + delete_all_nl_search_models: None, + create_nl_search_model: str, +) -> None: + """Test it can retrieve an NL search model from Typesense Server.""" + response = await actual_async_nl_search_models[create_nl_search_model].retrieve() + + assert_to_contain_keys( + response, + ["id", "model_name", "system_prompt", "max_bytes", "api_key"], + ) + assert response.get("id") == create_nl_search_model + + +@pytest.mark.open_ai +async def test_actual_update_async( + actual_async_nl_search_models: AsyncNLSearchModels, + delete_all_nl_search_models: None, + create_nl_search_model: str, +) -> None: + """Test that it can update an NL search model from Typesense Server.""" + response = await actual_async_nl_search_models[create_nl_search_model].update( + {"system_prompt": "This is a new system prompt for NL search"}, + ) + + assert_to_contain_keys( + response, + [ + "id", + "model_name", + "system_prompt", + "max_bytes", + "api_key", + ], + ) + + assert response.get("system_prompt") == "This is a new system prompt for NL search" + assert response.get("id") == create_nl_search_model + + +@pytest.mark.open_ai +async def test_actual_delete_async( + actual_async_nl_search_models: AsyncNLSearchModels, + delete_all_nl_search_models: None, + create_nl_search_model: str, +) -> None: + """Test that it can delete an NL search model from Typesense Server.""" + response = await actual_async_nl_search_models[create_nl_search_model].delete() + + assert_to_contain_keys( + response, + ["id"], + ) + + assert response.get("id") == create_nl_search_model diff --git a/tests/nl_search_models_test.py b/tests/nl_search_models_test.py index daaa842..e55f038 100644 --- a/tests/nl_search_models_test.py +++ b/tests/nl_search_models_test.py @@ -19,6 +19,8 @@ assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_nl_search_models import AsyncNLSearchModels from typesense.nl_search_models import NLSearchModels @@ -39,6 +41,23 @@ def test_init(fake_api_call: ApiCall) -> None: assert not nl_search_models.nl_search_models +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncNLSearchModels object is initialized correctly.""" + nl_search_models = AsyncNLSearchModels(fake_async_api_call) + + assert_match_object(nl_search_models.api_call, fake_async_api_call) + assert_object_lists_match( + nl_search_models.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + nl_search_models.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not nl_search_models.nl_search_models + + def test_get_missing_nl_search_model( fake_nl_search_models: NLSearchModels, ) -> None: @@ -63,6 +82,30 @@ def test_get_missing_nl_search_model( ) +def test_get_missing_nl_search_model_async( + fake_async_nl_search_models: AsyncNLSearchModels, +) -> None: + """Test that the AsyncNLSearchModels object can get a missing nl_search_model.""" + nl_search_model = fake_async_nl_search_models["nl_search_model_id"] + + assert_match_object( + nl_search_model.api_call, + fake_async_nl_search_models.api_call, + ) + assert_object_lists_match( + nl_search_model.api_call.node_manager.nodes, + fake_async_nl_search_models.api_call.node_manager.nodes, + ) + assert_match_object( + nl_search_model.api_call.config.nearest_node, + fake_async_nl_search_models.api_call.config.nearest_node, + ) + assert ( + nl_search_model._endpoint_path # noqa: WPS437 + == "/nl_search_models/nl_search_model_id" + ) + + def test_get_existing_nl_search_model( fake_nl_search_models: NLSearchModels, ) -> None: @@ -75,6 +118,18 @@ def test_get_existing_nl_search_model( assert nl_search_model is fetched_nl_search_model +def test_get_existing_nl_search_model_async( + fake_async_nl_search_models: AsyncNLSearchModels, +) -> None: + """Test that the AsyncNLSearchModels object can get an existing nl_search_model.""" + nl_search_model = fake_async_nl_search_models["nl_search_model_id"] + fetched_nl_search_model = fake_async_nl_search_models["nl_search_model_id"] + + assert len(fake_async_nl_search_models.nl_search_models) == 1 + + assert nl_search_model is fetched_nl_search_model + + @pytest.mark.open_ai def test_actual_create( actual_nl_search_models: NLSearchModels, @@ -114,3 +169,44 @@ def test_actual_retrieve( response[0], ["id", "api_key", "max_bytes", "model_name", "system_prompt"], ) + + +@pytest.mark.open_ai +async def test_actual_create_async( + actual_async_nl_search_models: AsyncNLSearchModels, +) -> None: + """Test that it can create an NL search model on Typesense Server.""" + response = await actual_async_nl_search_models.create( + { + "api_key": os.environ.get("OPEN_AI_KEY", "test-api-key"), + "max_bytes": 16384, + "model_name": "openai/gpt-3.5-turbo", + "system_prompt": "This is meant for testing purposes", + }, + ) + + assert_to_contain_keys( + response, + ["id", "api_key", "max_bytes", "model_name", "system_prompt"], + ) + + +@pytest.mark.open_ai +async def test_actual_retrieve_async( + actual_async_nl_search_models: AsyncNLSearchModels, + delete_all_nl_search_models: None, + create_nl_search_model: str, +) -> None: + """Test that it can retrieve NL search models from Typesense Server.""" + response = await actual_async_nl_search_models.retrieve() + assert len(response) == 1 + assert_to_contain_object( + response[0], + { + "id": create_nl_search_model, + }, + ) + assert_to_contain_keys( + response[0], + ["id", "api_key", "max_bytes", "model_name", "system_prompt"], + ) From cff9373ffdcd29b06ef5cdf066624226a791a478 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:12:31 +0200 Subject: [PATCH 23/32] feat(ops): add async support for operations - add AsyncOperations class for async operations functionality - add async tests for operations functionality - add async fixtures for testing async operations --- src/typesense/async_operations.py | 279 +++++++++++++++++++++++++++ tests/fixtures/operation_fixtures.py | 18 ++ tests/operations_test.py | 78 +++++++- 3 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 src/typesense/async_operations.py diff --git a/src/typesense/async_operations.py b/src/typesense/async_operations.py new file mode 100644 index 0000000..eb8e400 --- /dev/null +++ b/src/typesense/async_operations.py @@ -0,0 +1,279 @@ +""" +This module provides async functionality for performing various operations in the Typesense API. + +It contains the AsyncOperations class, which handles different API operations such as +health checks, snapshots, and configuration changes asynchronously. + +Classes: + AsyncOperations: Manages various async operations in the Typesense API. + +Dependencies: + - typesense.types.operations: + Provides type definitions for operation responses and parameters. + - typesense.async_api_call: Provides the AsyncApiCall class for making async API requests. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +from typesense.async_api_call import AsyncApiCall +from typesense.types.operations import ( + HealthCheckResponse, + LogSlowRequestsTimeParams, + OperationResponse, + SchemaChangesResponse, + SnapshotParameters, +) + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + + +class AsyncOperations: + """ + Manages various async operations in the Typesense API. + + This class provides async methods to perform different operations such as + health checks, snapshots, and configuration changes. + + Attributes: + resource_path (str): The base path for operations endpoints. + health_path (str): The path for the health check endpoint. + config_path (str): The path for the configuration endpoint. + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + + resource_path: typing.Final[str] = "/operations" + health_path: typing.Final[str] = "/health" + config_path: typing.Final[str] = "/config" + schema_changes: typing.Final[str] = "/schema_changes" + + def __init__(self, api_call: AsyncApiCall): + """ + Initialize the AsyncOperations instance. + + Args: + api_call (AsyncApiCall): The AsyncApiCall instance for making async API requests. + """ + self.api_call = api_call + + @typing.overload + async def perform( + self, + operation_name: typing.Literal["schema_changes"], + query_params: None = None, + ) -> typing.List[SchemaChangesResponse]: + """ + Perform a schema_changes operation. + + Args: + operation_name (Literal["schema_changes"]): The name of the operation. + query_params (None, optional): Query parameters (not used for schema_changes operation). + + Returns: + List[SchemaChangesResponse]: The response from the schema_changes operation. + """ + + @typing.overload + async def perform( + self, + operation_name: typing.Literal["vote"], + query_params: None = None, + ) -> OperationResponse: + """ + Perform a vote operation. + + Args: + operation_name (Literal["vote"]): The name of the operation. + query_params (None, optional): Query parameters (not used for vote operation). + + Returns: + OperationResponse: The response from the vote operation. + """ + + @typing.overload + async def perform( + self, + operation_name: typing.Literal["db/compact"], + query_params: None = None, + ) -> OperationResponse: + """ + Perform a database compaction operation. + + Args: + operation_name (Literal["db/compact"]): The name of the operation. + query_params (None, optional): Query parameters (not used for db/compact operation). + + Returns: + OperationResponse: The response from the database compaction operation. + """ + + @typing.overload + async def perform( + self, + operation_name: typing.Literal["cache/clear"], + query_params: None = None, + ) -> OperationResponse: + """ + Perform a cache clear operation. + + Args: + operation_name (Literal["cache/clear"]): The name of the operation. + query_params (None, optional): + Query parameters (not used for cache/clear operation). + + Returns: + OperationResponse: The response from the cache clear operation. + """ + + @typing.overload + async def perform( + self, + operation_name: str, + query_params: typing.Union[typing.Dict[str, str], None] = None, + ) -> OperationResponse: + """ + Perform a generic operation. + + Args: + operation_name (str): The name of the operation. + query_params (Union[Dict[str, str], None], optional): + Query parameters for the operation. + + Returns: + OperationResponse: The response from the operation. + """ + + @typing.overload + async def perform( + self, + operation_name: typing.Literal["snapshot"], + query_params: SnapshotParameters, + ) -> OperationResponse: + """ + Perform a snapshot operation. + + Args: + operation_name (Literal["snapshot"]): The name of the operation. + query_params (SnapshotParameters): Query parameters for the snapshot operation. + + Returns: + OperationResponse: The response from the snapshot operation. + """ + + async def perform( + self, + operation_name: typing.Union[ + typing.Literal[ + "snapshot", + "vote", + "db/compact", + "cache/clear", + "schema_changes", + ], + str, + ], + query_params: typing.Union[ + SnapshotParameters, + typing.Dict[str, str], + None, + ] = None, + ) -> OperationResponse: + """ + Perform an operation on the Typesense API. + + This method is the actual implementation for all the overloaded perform methods. + + Args: + operation_name (Literal["snapshot, vote, db/compact, cache/clear, schema_changes"]): + The name of the operation to perform. + query_params (Union[SnapshotParameters, Dict[str, str], None], optional): + Query parameters for the operation. + + Returns: + Union[OperationResponse, List[SchemaChangesResponse]]: + The response from the performed operation. + + Example: + >>> operations = AsyncOperations(async_api_call) + >>> response = await operations.perform("vote") + >>> health = await operations.is_healthy() + """ + response: OperationResponse = await self.api_call.post( + self._endpoint_path(operation_name), + params=query_params, + as_json=True, + entity_type=OperationResponse, + ) + return response + + async def is_healthy(self) -> bool: + """ + Check if the Typesense server is healthy. + + Returns: + bool: True if the server is healthy, False otherwise. + + Example: + >>> operations = AsyncOperations(async_api_call) + >>> healthy = await operations.is_healthy() + >>> print(healthy) + """ + call_resp: HealthCheckResponse = await self.api_call.get( + AsyncOperations.health_path, + as_json=True, + entity_type=HealthCheckResponse, + ) + if isinstance(call_resp, typing.Dict): + is_ok: bool = call_resp.get("ok", False) + else: + is_ok = False + return is_ok + + async def toggle_slow_request_log( + self, + log_slow_requests_time_params: LogSlowRequestsTimeParams, + ) -> typing.Dict[str, typing.Union[str, bool]]: + """ + Toggle the slow request log configuration. + + Args: + log_slow_requests_time_params (LogSlowRequestsTimeParams): + Parameters for configuring slow request logging. + + Returns: + Dict[str, Union[str, bool]]: The response from the configuration change operation. + + Example: + >>> operations = AsyncOperations(async_api_call) + >>> response = await operations.toggle_slow_request_log( + ... {"log_slow_requests_time_ms": 100} + ... ) + """ + data_dashed = { + key.replace("_", "-"): dashed_value + for key, dashed_value in log_slow_requests_time_params.items() + } + response: typing.Dict[str, typing.Union[str, bool]] = await self.api_call.post( + AsyncOperations.config_path, + as_json=True, + entity_type=typing.Dict[str, typing.Union[str, bool]], + body=data_dashed, + ) + return response + + @staticmethod + def _endpoint_path(operation_name: str) -> str: + """ + Generate the endpoint path for a given operation. + + Args: + operation_name (str): The name of the operation. + + Returns: + str: The full endpoint path for the operation. + """ + return "/".join([AsyncOperations.resource_path, operation_name]) diff --git a/tests/fixtures/operation_fixtures.py b/tests/fixtures/operation_fixtures.py index 6391ad8..f72c505 100644 --- a/tests/fixtures/operation_fixtures.py +++ b/tests/fixtures/operation_fixtures.py @@ -3,6 +3,8 @@ import pytest from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_operations import AsyncOperations from typesense.operations import Operations @@ -16,3 +18,19 @@ def actual_operations_fixture(actual_api_call: ApiCall) -> Operations: def fake_operations_fixture(fake_api_call: ApiCall) -> Operations: """Return a Collection object with test values.""" return Operations(fake_api_call) + + +@pytest.fixture(scope="function", name="actual_async_operations") +def actual_async_operations_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncOperations: + """Return a AsyncOperations object using a real API.""" + return AsyncOperations(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_operations") +def fake_async_operations_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncOperations: + """Return a AsyncOperations object with test values.""" + return AsyncOperations(fake_async_api_call) diff --git a/tests/operations_test.py b/tests/operations_test.py index c6cfaf7..14b28a9 100644 --- a/tests/operations_test.py +++ b/tests/operations_test.py @@ -6,12 +6,14 @@ from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_operations import AsyncOperations from typesense.exceptions import ObjectNotFound from typesense.operations import Operations def test_init(fake_api_call: ApiCall) -> None: - """Test that the Override object is initialized correctly.""" + """Test that the Operations object is initialized correctly.""" operations = Operations(fake_api_call) assert_match_object(operations.api_call, fake_api_call) @@ -28,6 +30,24 @@ def test_init(fake_api_call: ApiCall) -> None: ) +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncOperations object is initialized correctly.""" + operations = AsyncOperations(fake_async_api_call) + + assert_match_object(operations.api_call, fake_async_api_call) + assert_object_lists_match( + operations.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + operations.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert ( + operations._endpoint_path("resource") == "/operations/resource" # noqa: WPS437 + ) + + def test_vote(actual_operations: Operations) -> None: """Test that the Operations object can perform the vote operation.""" response = actual_operations.perform("vote") @@ -80,3 +100,59 @@ def test_invalid_operation(actual_operations: Operations) -> None: """Test that the Operations object throws an error for an invalid operation.""" with pytest.raises(ObjectNotFound): actual_operations.perform("invalid") + + +async def test_vote_async(actual_async_operations: AsyncOperations) -> None: + """Test that the AsyncOperations object can perform the vote operation.""" + response = await actual_async_operations.perform("vote") + + # It will error on single node clusters if asserted to True + assert response["success"] is not None + + +async def test_db_compact_async(actual_async_operations: AsyncOperations) -> None: + """Test that the AsyncOperations object can perform the db/compact operation.""" + response = await actual_async_operations.perform("db/compact") + + assert response["success"] + + +async def test_cache_clear_async(actual_async_operations: AsyncOperations) -> None: + """Test that the AsyncOperations object can perform the cache/clear operation.""" + response = await actual_async_operations.perform("cache/clear") + + assert response["success"] + + +async def test_snapshot_async(actual_async_operations: AsyncOperations) -> None: + """Test that the AsyncOperations object can perform the snapshot operation.""" + response = await actual_async_operations.perform( + "snapshot", + {"snapshot_path": "/tmp"}, # noqa: S108 + ) + + assert response["success"] + + +async def test_health_async(actual_async_operations: AsyncOperations) -> None: + """Test that the AsyncOperations object can perform the health operation.""" + response = await actual_async_operations.is_healthy() + + assert response + + +async def test_log_slow_requests_time_ms_async( + actual_async_operations: AsyncOperations, +) -> None: + """Test that the AsyncOperations object can perform the log_slow_requests_time_ms operation.""" + response = await actual_async_operations.toggle_slow_request_log( + {"log_slow_requests_time_ms": 100}, + ) + + assert response["success"] + + +async def test_invalid_operation_async(actual_async_operations: AsyncOperations) -> None: + """Test that the AsyncOperations object throws an error for an invalid operation.""" + with pytest.raises(ObjectNotFound): + await actual_async_operations.perform("invalid") From fc0f2055d2f51e50c85ea5325310ff158efa3940 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:16:29 +0200 Subject: [PATCH 24/32] feat(overrides): add async support for override operations - add AsyncOverride class for async individual override operations - add AsyncOverrides class for async overrides collection operations - add async tests for override and overrides functionality - add async fixtures for testing async override operations --- src/typesense/async_override.py | 112 ++++++++++++++++++++ src/typesense/async_overrides.py | 157 ++++++++++++++++++++++++++++ tests/fixtures/override_fixtures.py | 25 +++++ tests/override_test.py | 60 +++++++++++ tests/overrides_test.py | 128 +++++++++++++++++++++++ 5 files changed, 482 insertions(+) create mode 100644 src/typesense/async_override.py create mode 100644 src/typesense/async_overrides.py diff --git a/src/typesense/async_override.py b/src/typesense/async_override.py new file mode 100644 index 0000000..8948ee3 --- /dev/null +++ b/src/typesense/async_override.py @@ -0,0 +1,112 @@ +""" +This module provides async functionality for managing individual overrides in Typesense. + +Classes: + - AsyncOverride: Handles async operations related to a specific override within a collection. + +Methods: + - __init__: Initializes the AsyncOverride object. + - retrieve: Retrieves the details of this specific override. + - delete: Deletes this specific override. + +Attributes: + - _endpoint_path: The API endpoint path for this specific override. + +The AsyncOverride class interacts with the Typesense API to manage operations on a +specific override within a collection. It provides methods to retrieve and delete +individual overrides. + +For more information regarding Overrides, refer to the Curation [documentation] +(https://typesense.org/docs/27.0/api/curation.html#curation). + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.logger import warn_deprecation +from typesense.types.override import OverrideDeleteSchema, OverrideSchema + + +class AsyncOverride: + """ + Class for managing individual overrides in a Typesense collection (async). + + This class provides methods to interact with a specific override, + including retrieving and deleting it. + + Attributes: + api_call (AsyncApiCall): The API call object for making requests. + collection_name (str): The name of the collection. + override_id (str): The ID of the override. + """ + + def __init__( + self, + api_call: AsyncApiCall, + collection_name: str, + override_id: str, + ) -> None: + """ + Initialize the AsyncOverride object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + collection_name (str): The name of the collection. + override_id (str): The ID of the override. + """ + self.api_call = api_call + self.collection_name = collection_name + self.override_id = override_id + + async def retrieve(self) -> OverrideSchema: + """ + Retrieve this specific override. + + Returns: + OverrideSchema: The schema containing the override details. + """ + response: OverrideSchema = await self.api_call.get( + self._endpoint_path, + entity_type=OverrideSchema, + as_json=True, + ) + return response + + async def delete(self) -> OverrideDeleteSchema: + """ + Delete this specific override. + + Returns: + OverrideDeleteSchema: The schema containing the deletion response. + """ + response: OverrideDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=OverrideDeleteSchema, + ) + return response + + @property + @warn_deprecation( # type: ignore[untyped-decorator] + "The override API (collections/{collection}/overrides/{override_id}) is deprecated is removed on v30+. " + "Use curation sets (curation_sets) instead.", + flag_name="overrides_deprecation", + ) + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific override. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_collections import AsyncCollections + from typesense.async_overrides import AsyncOverrides + + return "/".join( + [ + AsyncCollections.resource_path, + self.collection_name, + AsyncOverrides.resource_path, + self.override_id, + ], + ) diff --git a/src/typesense/async_overrides.py b/src/typesense/async_overrides.py new file mode 100644 index 0000000..2f3fe2d --- /dev/null +++ b/src/typesense/async_overrides.py @@ -0,0 +1,157 @@ +""" +This module provides async functionality for managing overrides in Typesense. + +Classes: + - AsyncOverrides: Handles async operations related to overrides within a collection. + +Methods: + - __init__: Initializes the AsyncOverrides object. + - __getitem__: Retrieves or creates an AsyncOverride object for a given override_id. + - _endpoint_path: Constructs the API endpoint path for override operations. + - upsert: Creates or updates an override. + - retrieve: Retrieves all overrides for the collection. + +Attributes: + - RESOURCE_PATH: The API resource path for overrides. + +The AsyncOverrides class interacts with the Typesense API to manage override operations +within a specific collection. It provides methods to create, update, and retrieve +overrides, as well as access individual AsyncOverride objects. + +For more information regarding Overrides, refer to the Curation [documentation] +(https://typesense.org/docs/27.0/api/curation.html#curation). + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +import sys + +from typing_extensions import deprecated + +from typesense.async_api_call import AsyncApiCall +from typesense.async_override import AsyncOverride +from typesense.logger import warn_deprecation +from typesense.types.override import ( + OverrideCreateSchema, + OverrideRetrieveSchema, + OverrideSchema, +) + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + + +@deprecated("AsyncOverrides is deprecated on v30+. Use client.curation_sets instead.") +class AsyncOverrides: + """ + Class for managing overrides in a Typesense collection (async). + + This class provides methods to interact with overrides, including + retrieving, creating, and updating them. + + Attributes: + RESOURCE_PATH (str): The API resource path for overrides. + api_call (AsyncApiCall): The API call object for making requests. + collection_name (str): The name of the collection. + overrides (Dict[str, AsyncOverride]): A dictionary of AsyncOverride objects. + """ + + resource_path: typing.Final[str] = "overrides" + + def __init__( + self, + api_call: AsyncApiCall, + collection_name: str, + ) -> None: + """ + Initialize the AsyncOverrides object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + collection_name (str): The name of the collection. + """ + self.api_call = api_call + self.collection_name = collection_name + self.overrides: typing.Dict[str, AsyncOverride] = {} + + def __getitem__(self, override_id: str) -> AsyncOverride: + """ + Get or create an AsyncOverride object for a given override_id. + + Args: + override_id (str): The ID of the override. + + Returns: + AsyncOverride: The AsyncOverride object for the given ID. + """ + if not self.overrides.get(override_id): + self.overrides[override_id] = AsyncOverride( + self.api_call, + self.collection_name, + override_id, + ) + return self.overrides[override_id] + + async def upsert( + self, override_id: str, schema: OverrideCreateSchema + ) -> OverrideSchema: + """ + Create or update an override. + + Args: + id (str): The ID of the override. + schema (OverrideCreateSchema): The schema for creating or updating the override. + + Returns: + OverrideSchema: The created or updated override. + """ + response: OverrideSchema = await self.api_call.put( + endpoint=self._endpoint_path(override_id), + entity_type=OverrideSchema, + body=schema, + ) + return response + + async def retrieve(self) -> OverrideRetrieveSchema: + """ + Retrieve all overrides for the collection. + + Returns: + OverrideRetrieveSchema: The schema containing all overrides. + """ + response: OverrideRetrieveSchema = await self.api_call.get( + self._endpoint_path(), + entity_type=OverrideRetrieveSchema, + as_json=True, + ) + return response + + @warn_deprecation( # type: ignore[untyped-decorator] + "AsyncOverrides is deprecated on v30+. Use client.curation_sets instead.", + flag_name="overrides_deprecation", + ) + def _endpoint_path(self, override_id: typing.Union[str, None] = None) -> str: + """ + Construct the API endpoint path for override operations. + + Args: + override_id (Union[str, None], optional): The ID of the override. Defaults to None. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_collections import AsyncCollections + + override_id = override_id or "" + + return "/".join( + [ + AsyncCollections.resource_path, + self.collection_name, + AsyncOverrides.resource_path, + override_id, + ], + ) diff --git a/tests/fixtures/override_fixtures.py b/tests/fixtures/override_fixtures.py index d584bbe..f810c1f 100644 --- a/tests/fixtures/override_fixtures.py +++ b/tests/fixtures/override_fixtures.py @@ -4,6 +4,9 @@ import requests from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_override import AsyncOverride +from typesense.async_overrides import AsyncOverrides from typesense.override import Override from typesense.overrides import Overrides @@ -38,3 +41,25 @@ def fake_overrides_fixture(fake_api_call: ApiCall) -> Overrides: def fake_override_fixture(fake_api_call: ApiCall) -> Override: """Return a Override object with test values.""" return Override(fake_api_call, "companies", "company_override") + + +@pytest.fixture(scope="function", name="actual_async_overrides") +def actual_async_overrides_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncOverrides: + """Return a AsyncOverrides object using a real API.""" + return AsyncOverrides(actual_async_api_call, "companies") + + +@pytest.fixture(scope="function", name="fake_async_overrides") +def fake_async_overrides_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncOverrides: + """Return a AsyncOverrides object with test values.""" + return AsyncOverrides(fake_async_api_call, "companies") + + +@pytest.fixture(scope="function", name="fake_async_override") +def fake_async_override_fixture(fake_async_api_call: AsyncApiCall) -> AsyncOverride: + """Return a AsyncOverride object with test values.""" + return AsyncOverride(fake_async_api_call, "companies", "company_override") diff --git a/tests/override_test.py b/tests/override_test.py index b47619c..c411134 100644 --- a/tests/override_test.py +++ b/tests/override_test.py @@ -10,6 +10,9 @@ assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_collections import AsyncCollections +from typesense.async_override import AsyncOverride from typesense.collections import Collections from typesense.override import Override, OverrideDeleteSchema from typesense.types.override import OverrideSchema @@ -85,3 +88,60 @@ def test_actual_delete( response = actual_collections["companies"].overrides["company_override"].delete() assert response == {"id": "company_override"} + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncOverride object is initialized correctly.""" + override = AsyncOverride(fake_async_api_call, "companies", "company_override") + + assert override.collection_name == "companies" + assert override.override_id == "company_override" + assert_match_object(override.api_call, fake_async_api_call) + assert_object_lists_match( + override.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + override.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert ( + override._endpoint_path() # noqa: WPS437 + == "/collections/companies/overrides/company_override" + ) + + +async def test_actual_retrieve_async( + actual_async_collections: AsyncCollections, + delete_all: None, + create_override: None, +) -> None: + """Test that the AsyncOverride object can retrieve an override from Typesense Server.""" + response = await actual_async_collections["companies"].overrides["company_override"].retrieve() + + assert response["rule"] == { + "match": "exact", + "query": "companies", + } + assert response["filter_by"] == "num_employees>10" + assert_to_contain_object( + response, + { + "rule": { + "match": "exact", + "query": "companies", + }, + "filter_by": "num_employees>10", + }, + ) + + +async def test_actual_delete_async( + actual_async_collections: AsyncCollections, + delete_all: None, + create_override: None, +) -> None: + """Test that the AsyncOverride object can delete an override from Typesense Server.""" + response = await actual_async_collections["companies"].overrides["company_override"].delete() + + assert response == {"id": "company_override"} diff --git a/tests/overrides_test.py b/tests/overrides_test.py index cd6fe32..20347f6 100644 --- a/tests/overrides_test.py +++ b/tests/overrides_test.py @@ -10,6 +10,8 @@ assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_collections import AsyncCollections from typesense.collections import Collections from typesense.overrides import OverrideRetrieveSchema, Overrides, OverrideSchema from tests.utils.version import is_v30_or_above @@ -148,3 +150,129 @@ def test_actual_retrieve( "filter_by": "num_employees>10", }, ) + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncOverrides object is initialized correctly.""" + from typesense.async_overrides import AsyncOverrides + + overrides = AsyncOverrides(fake_async_api_call, "companies") + + assert_match_object(overrides.api_call, fake_async_api_call) + assert_object_lists_match( + overrides.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + overrides.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not overrides.overrides + + +def test_get_missing_override_async(fake_async_overrides) -> None: + """Test that the AsyncOverrides object can get a missing override.""" + from typesense.async_overrides import AsyncOverrides + + override = fake_async_overrides["company_override"] + + assert override.override_id == "company_override" + assert_match_object(override.api_call, fake_async_overrides.api_call) + assert_object_lists_match( + override.api_call.node_manager.nodes, fake_async_overrides.api_call.node_manager.nodes + ) + assert_match_object( + override.api_call.config.nearest_node, + fake_async_overrides.api_call.config.nearest_node, + ) + assert override.collection_name == "companies" + assert ( + override._endpoint_path() # noqa: WPS437 + == "/collections/companies/overrides/company_override" + ) + + +def test_get_existing_override_async(fake_async_overrides) -> None: + """Test that the AsyncOverrides object can get an existing override.""" + override = fake_async_overrides["companies"] + fetched_override = fake_async_overrides["companies"] + + assert len(fake_async_overrides.overrides) == 1 + + assert override is fetched_override + + +async def test_actual_create_async( + actual_async_overrides, + delete_all: None, + create_collection: None, +) -> None: + """Test that the AsyncOverrides object can create an override on Typesense Server.""" + response = await actual_async_overrides.upsert( + "company_override", + { + "rule": {"match": "exact", "query": "companies"}, + "filter_by": "num_employees>10", + }, + ) + + assert response == { + "id": "company_override", + "rule": {"match": "exact", "query": "companies"}, + "filter_by": "num_employees>10", + } + + +async def test_actual_update_async( + actual_async_overrides, + delete_all: None, + create_collection: None, +) -> None: + """Test that the AsyncOverrides object can update an override on Typesense Server.""" + create_response = await actual_async_overrides.upsert( + "company_override", + { + "rule": {"match": "exact", "query": "companies"}, + "filter_by": "num_employees>10", + }, + ) + + assert create_response == { + "id": "company_override", + "rule": {"match": "exact", "query": "companies"}, + "filter_by": "num_employees>10", + } + + update_response = await actual_async_overrides.upsert( + "company_override", + { + "rule": {"match": "contains", "query": "companies"}, + "filter_by": "num_employees>20", + }, + ) + + assert update_response == { + "id": "company_override", + "rule": {"match": "contains", "query": "companies"}, + "filter_by": "num_employees>20", + } + + +async def test_actual_retrieve_async( + delete_all: None, + create_override: None, + actual_async_collections: AsyncCollections, +) -> None: + """Test that the AsyncOverrides object can retrieve an override from Typesense Server.""" + response = await actual_async_collections["companies"].overrides.retrieve() + + assert len(response["overrides"]) == 1 + assert_to_contain_object( + response["overrides"][0], + { + "id": "company_override", + "rule": {"match": "exact", "query": "companies"}, + "filter_by": "num_employees>10", + }, + ) From 0487a9a3c0df5aab05092f3c16fdcf814021f2a0 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:17:17 +0200 Subject: [PATCH 25/32] feat(synonyms): add async support for synonym operations - add AsyncSynonym class for async individual synonym operations - add AsyncSynonyms class for async synonyms collection operations - add async tests for synonym and synonyms functionality - add async fixtures for testing async synonym operations - remove future annotations imports from test files --- src/typesense/async_synonym.py | 104 ++++++++++++++++++++ src/typesense/async_synonyms.py | 152 +++++++++++++++++++++++++++++ tests/fixtures/synonym_fixtures.py | 25 +++++ tests/synonym_test.py | 65 +++++++++++- tests/synonyms_test.py | 115 ++++++++++++++++++++++ 5 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 src/typesense/async_synonym.py create mode 100644 src/typesense/async_synonyms.py diff --git a/src/typesense/async_synonym.py b/src/typesense/async_synonym.py new file mode 100644 index 0000000..24f6bbb --- /dev/null +++ b/src/typesense/async_synonym.py @@ -0,0 +1,104 @@ +""" +This module provides async functionality for managing individual synonyms in Typesense. + +Classes: + - AsyncSynonym: Handles async operations related to a specific synonym within a collection. + +Methods: + - __init__: Initializes the AsyncSynonym object. + - _endpoint_path: Constructs the API endpoint path for this specific synonym. + - retrieve: Retrieves the details of this specific synonym. + - delete: Deletes this specific synonym. + +The AsyncSynonym class interacts with the Typesense API to manage operations on a +specific synonym within a collection. It provides methods to retrieve and delete +individual synonyms. + +For more information regarding Synonyms, refer to the Synonyms [documentation] +(https://typesense.org/docs/27.0/api/synonyms.html#synonyms). + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.logger import warn_deprecation +from typesense.types.synonym import SynonymDeleteSchema, SynonymSchema + + +class AsyncSynonym: + """ + Class for managing individual synonyms in a Typesense collection (async). + + This class provides methods to interact with a specific synonym, + including retrieving and deleting it. + + Attributes: + api_call (AsyncApiCall): The API call object for making requests. + collection_name (str): The name of the collection. + synonym_id (str): The ID of the synonym. + """ + + def __init__( + self, + api_call: AsyncApiCall, + collection_name: str, + synonym_id: str, + ) -> None: + """ + Initialize the AsyncSynonym object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + collection_name (str): The name of the collection. + synonym_id (str): The ID of the synonym. + """ + self.api_call = api_call + self.collection_name = collection_name + self.synonym_id = synonym_id + + async def retrieve(self) -> SynonymSchema: + """ + Retrieve this specific synonym. + + Returns: + SynonymSchema: The schema containing the synonym details. + """ + return await self.api_call.get(self._endpoint_path, entity_type=SynonymSchema) + + async def delete(self) -> SynonymDeleteSchema: + """ + Delete this specific synonym. + + Returns: + SynonymDeleteSchema: The schema containing the deletion response. + """ + return await self.api_call.delete( + self._endpoint_path, + entity_type=SynonymDeleteSchema, + ) + + @property + @warn_deprecation( # type: ignore[untyped-decorator] + "The synonym API (collections/{collection}/synonyms/{synonym_id}) is deprecated is removed on v30+. " + "Use synonym sets (synonym_sets) instead.", + flag_name="synonyms_deprecation", + ) + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific synonym. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_collections import AsyncCollections + from typesense.async_synonyms import AsyncSynonyms + + return "/".join( + [ + AsyncCollections.resource_path, + self.collection_name, + AsyncSynonyms.resource_path, + self.synonym_id, + ], + ) diff --git a/src/typesense/async_synonyms.py b/src/typesense/async_synonyms.py new file mode 100644 index 0000000..bc0bd77 --- /dev/null +++ b/src/typesense/async_synonyms.py @@ -0,0 +1,152 @@ +""" +This module provides async functionality for managing synonyms in Typesense. + +Classes: + - AsyncSynonyms: Handles async operations related to synonyms within a collection. + +Methods: + - __init__: Initializes the AsyncSynonyms object. + - __getitem__: Retrieves or creates an AsyncSynonym object for a given synonym_id. + - _endpoint_path: Constructs the API endpoint path for synonym operations. + - upsert: Creates or updates a synonym. + - retrieve: Retrieves all synonyms for the collection. + +Attributes: + - RESOURCE_PATH: The API resource path for synonyms. + +The AsyncSynonyms class interacts with the Typesense API to manage synonym operations +within a specific collection. It provides methods to create, update, and retrieve +synonyms, as well as access individual AsyncSynonym objects. + +For more information regarding Synonyms, refer to the Synonyms [documentation] +(https://typesense.org/docs/27.0/api/synonyms.html#synonyms). + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +import sys + +from typing_extensions import deprecated + +from typesense.async_api_call import AsyncApiCall +from typesense.async_synonym import AsyncSynonym +from typesense.logger import warn_deprecation +from typesense.types.synonym import ( + SynonymCreateSchema, + SynonymSchema, + SynonymsRetrieveSchema, +) + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + + +@deprecated("AsyncSynonyms is deprecated on v30+. Use client.synonym_sets instead.") +class AsyncSynonyms: + """ + Class for managing synonyms in a Typesense collection (async). + + This class provides methods to interact with synonyms, including + retrieving, creating, and updating them. + + Attributes: + RESOURCE_PATH (str): The API resource path for synonyms. + api_call (AsyncApiCall): The API call object for making requests. + collection_name (str): The name of the collection. + synonyms (Dict[str, AsyncSynonym]): A dictionary of AsyncSynonym objects. + """ + + resource_path: typing.Final[str] = "synonyms" + + def __init__(self, api_call: AsyncApiCall, collection_name: str) -> None: + """ + Initialize the AsyncSynonyms object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + collection_name (str): The name of the collection. + """ + self.api_call = api_call + self.collection_name = collection_name + self.synonyms: typing.Dict[str, AsyncSynonym] = {} + + def __getitem__(self, synonym_id: str) -> AsyncSynonym: + """ + Get or create an AsyncSynonym object for a given synonym_id. + + Args: + synonym_id (str): The ID of the synonym. + + Returns: + AsyncSynonym: The AsyncSynonym object for the given ID. + """ + if not self.synonyms.get(synonym_id): + self.synonyms[synonym_id] = AsyncSynonym( + self.api_call, + self.collection_name, + synonym_id, + ) + return self.synonyms[synonym_id] + + async def upsert( + self, synonym_id: str, schema: SynonymCreateSchema + ) -> SynonymSchema: + """ + Create or update a synonym. + + Args: + id (str): The ID of the synonym. + schema (SynonymCreateSchema): The schema for creating or updating the synonym. + + Returns: + SynonymSchema: The created or updated synonym. + """ + response = await self.api_call.put( + self._endpoint_path(synonym_id), + body=schema, + entity_type=SynonymSchema, + ) + return response + + async def retrieve(self) -> SynonymsRetrieveSchema: + """ + Retrieve all synonyms for the collection. + + Returns: + SynonymsRetrieveSchema: The schema containing all synonyms. + """ + response = await self.api_call.get( + self._endpoint_path(), + entity_type=SynonymsRetrieveSchema, + ) + return response + + @warn_deprecation( # type: ignore[untyped-decorator] + "The synonyms API (collections/{collection}/synonyms) is deprecated is removed on v30+. " + "Use synonym sets (synonym_sets) instead.", + flag_name="synonyms_deprecation", + ) + def _endpoint_path(self, synonym_id: typing.Union[str, None] = None) -> str: + """ + Construct the API endpoint path for synonym operations. + + Args: + synonym_id (Union[str, None], optional): The ID of the synonym. Defaults to None. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_collections import AsyncCollections + + synonym_id = synonym_id or "" + return "/".join( + [ + AsyncCollections.resource_path, + self.collection_name, + AsyncSynonyms.resource_path, + synonym_id, + ], + ) diff --git a/tests/fixtures/synonym_fixtures.py b/tests/fixtures/synonym_fixtures.py index 8387cfa..731831e 100644 --- a/tests/fixtures/synonym_fixtures.py +++ b/tests/fixtures/synonym_fixtures.py @@ -4,6 +4,9 @@ import requests from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_synonym import AsyncSynonym +from typesense.async_synonyms import AsyncSynonyms from typesense.synonym import Synonym from typesense.synonyms import Synonyms @@ -42,3 +45,25 @@ def actual_synonyms_fixture(actual_api_call: ApiCall) -> Synonyms: def fake_synonym_fixture(fake_api_call: ApiCall) -> Synonym: """Return a Synonym object with test values.""" return Synonym(fake_api_call, "companies", "company_synonym") + + +@pytest.fixture(scope="function", name="actual_async_synonyms") +def actual_async_synonyms_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncSynonyms: + """Return a AsyncSynonyms object using a real API.""" + return AsyncSynonyms(actual_async_api_call, "companies") + + +@pytest.fixture(scope="function", name="fake_async_synonyms") +def fake_async_synonyms_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncSynonyms: + """Return a AsyncSynonyms object with test values.""" + return AsyncSynonyms(fake_async_api_call, "companies") + + +@pytest.fixture(scope="function", name="fake_async_synonym") +def fake_async_synonym_fixture(fake_async_api_call: AsyncApiCall) -> AsyncSynonym: + """Return a AsyncSynonym object with test values.""" + return AsyncSynonym(fake_async_api_call, "companies", "company_synonym") diff --git a/tests/synonym_test.py b/tests/synonym_test.py index d2d2ada..05beefa 100644 --- a/tests/synonym_test.py +++ b/tests/synonym_test.py @@ -1,7 +1,5 @@ """Tests for the Synonym class.""" -from __future__ import annotations - import pytest from tests.utils.object_assertions import ( @@ -11,8 +9,11 @@ ) from tests.utils.version import is_v30_or_above from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_collections import AsyncCollections from typesense.collections import Collections from typesense.client import Client +from typesense.synonym import Synonym pytestmark = pytest.mark.skipif( @@ -78,3 +79,63 @@ def test_actual_delete( response = actual_collections["companies"].synonyms["company_synonym"].delete() assert response == {"id": "company_synonym"} + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncSynonym object is initialized correctly.""" + from typesense.async_synonym import AsyncSynonym + + synonym = AsyncSynonym(fake_async_api_call, "companies", "company_synonym") + + assert synonym.collection_name == "companies" + assert synonym.synonym_id == "company_synonym" + assert_match_object(synonym.api_call, fake_async_api_call) + assert_object_lists_match( + synonym.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + synonym.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert ( + synonym._endpoint_path() # noqa: WPS437 + == "/collections/companies/synonyms/company_synonym" + ) + + +async def test_actual_retrieve_async( + actual_async_collections: AsyncCollections, + delete_all: None, + create_synonym: None, +) -> None: + """Test that the AsyncSynonym object can retrieve an synonym from Typesense Server.""" + response = ( + await actual_async_collections["companies"] + .synonyms["company_synonym"] + .retrieve() + ) + + assert response["id"] == "company_synonym" + + assert response["synonyms"] == ["companies", "corporations", "firms"] + assert_to_contain_object( + response, + { + "id": "company_synonym", + "synonyms": ["companies", "corporations", "firms"], + }, + ) + + +async def test_actual_delete_async( + actual_async_collections: AsyncCollections, + delete_all: None, + create_synonym: None, +) -> None: + """Test that the AsyncSynonym object can delete an synonym from Typesense Server.""" + response = ( + await actual_async_collections["companies"].synonyms["company_synonym"].delete() + ) + + assert response == {"id": "company_synonym"} diff --git a/tests/synonyms_test.py b/tests/synonyms_test.py index e8c4b05..a300c44 100644 --- a/tests/synonyms_test.py +++ b/tests/synonyms_test.py @@ -10,6 +10,8 @@ assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_collections import AsyncCollections from typesense.collections import Collections from tests.utils.version import is_v30_or_above from typesense.client import Client @@ -136,3 +138,116 @@ def test_actual_retrieve( "synonyms": ["companies", "corporations", "firms"], }, ) + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncSynonyms object is initialized correctly.""" + from typesense.async_synonyms import AsyncSynonyms + + synonyms = AsyncSynonyms(fake_async_api_call, "companies") + + assert_match_object(synonyms.api_call, fake_async_api_call) + assert_object_lists_match( + synonyms.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + synonyms.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not synonyms.synonyms + + +def test_get_missing_synonym_async(fake_async_synonyms) -> None: + """Test that the AsyncSynonyms object can get a missing synonym.""" + from typesense.async_synonyms import AsyncSynonyms + + synonym = fake_async_synonyms["company_synonym"] + + assert synonym.synonym_id == "company_synonym" + assert_match_object(synonym.api_call, fake_async_synonyms.api_call) + assert_object_lists_match( + synonym.api_call.node_manager.nodes, fake_async_synonyms.api_call.node_manager.nodes + ) + assert_match_object( + synonym.api_call.config.nearest_node, + fake_async_synonyms.api_call.config.nearest_node, + ) + assert synonym.collection_name == "companies" + assert ( + synonym._endpoint_path() # noqa: WPS437 + == "/collections/companies/synonyms/company_synonym" + ) + + +def test_get_existing_synonym_async(fake_async_synonyms) -> None: + """Test that the AsyncSynonyms object can get an existing synonym.""" + synonym = fake_async_synonyms["companies"] + fetched_synonym = fake_async_synonyms["companies"] + + assert len(fake_async_synonyms.synonyms) == 1 + + assert synonym is fetched_synonym + + +async def test_actual_create_async( + actual_async_synonyms, + delete_all: None, + create_collection: None, +) -> None: + """Test that the AsyncSynonyms object can create an synonym on Typesense Server.""" + response = await actual_async_synonyms.upsert( + "company_synonym", + {"synonyms": ["companies", "corporations", "firms"]}, + ) + + assert response == { + "id": "company_synonym", + "synonyms": ["companies", "corporations", "firms"], + } + + +async def test_actual_update_async( + actual_async_synonyms, + delete_all: None, + create_collection: None, +) -> None: + """Test that the AsyncSynonyms object can update an synonym on Typesense Server.""" + create_response = await actual_async_synonyms.upsert( + "company_synonym", + {"synonyms": ["companies", "corporations", "firms"]}, + ) + + assert create_response == { + "id": "company_synonym", + "synonyms": ["companies", "corporations", "firms"], + } + + update_response = await actual_async_synonyms.upsert( + "company_synonym", + {"synonyms": ["companies", "corporations"]}, + ) + + assert update_response == { + "id": "company_synonym", + "synonyms": ["companies", "corporations"], + } + + +async def test_actual_retrieve_async( + delete_all: None, + create_synonym: None, + actual_async_collections: AsyncCollections, +) -> None: + """Test that the AsyncSynonyms object can retrieve an synonym from Typesense Server.""" + response = await actual_async_collections["companies"].synonyms.retrieve() + + assert len(response["synonyms"]) == 1 + assert_to_contain_object( + response["synonyms"][0], + { + "id": "company_synonym", + "synonyms": ["companies", "corporations", "firms"], + }, + ) From 723b6dca9e28011f399454201a2f660d78673e48 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:17:43 +0200 Subject: [PATCH 26/32] feat(synonym-set): add async support for synonym set operations - add AsyncSynonymSet class for async individual synonym set operations - add AsyncSynonymSets class for async synonym sets collection operations - add async tests for synonym set and sets functionality - add async fixtures for testing async synonym set operations - remove future annotations imports from test files --- src/typesense/async_synonym_set.py | 102 +++++++++++++++++++++++++ src/typesense/async_synonym_sets.py | 34 +++++++++ tests/fixtures/synonym_set_fixtures.py | 31 ++++++++ tests/synonym_set_items_test.py | 66 ++++++++++++++++ tests/synonym_set_test.py | 51 +++++++++++++ tests/synonym_sets_test.py | 66 +++++++++++++++- 6 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 src/typesense/async_synonym_set.py create mode 100644 src/typesense/async_synonym_sets.py diff --git a/src/typesense/async_synonym_set.py b/src/typesense/async_synonym_set.py new file mode 100644 index 0000000..d850f6b --- /dev/null +++ b/src/typesense/async_synonym_set.py @@ -0,0 +1,102 @@ +"""Client for single Synonym Set operations (async).""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.types.synonym_set import ( + SynonymItemDeleteSchema, + SynonymItemSchema, + SynonymSetCreateSchema, + SynonymSetDeleteSchema, + SynonymSetRetrieveSchema, +) + + +class AsyncSynonymSet: + def __init__(self, api_call: AsyncApiCall, name: str) -> None: + self.api_call = api_call + self.name = name + + @property + def _endpoint_path(self) -> str: + from typesense.async_synonym_sets import AsyncSynonymSets + + return "/".join([AsyncSynonymSets.resource_path, self.name]) + + async def retrieve(self) -> SynonymSetRetrieveSchema: + response: SynonymSetRetrieveSchema = await self.api_call.get( + self._endpoint_path, + as_json=True, + entity_type=SynonymSetRetrieveSchema, + ) + return response + + async def upsert(self, set: SynonymSetCreateSchema) -> SynonymSetCreateSchema: + response: SynonymSetCreateSchema = await self.api_call.put( + self._endpoint_path, + entity_type=SynonymSetCreateSchema, + body=set, + ) + return response + + async def delete(self) -> SynonymSetDeleteSchema: + response: SynonymSetDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=SynonymSetDeleteSchema, + ) + return response + + @property + def _items_path(self) -> str: + return "/".join([self._endpoint_path, "items"]) # /synonym_sets/{name}/items + + async def list_items( + self, + *, + limit: typing.Union[int, None] = None, + offset: typing.Union[int, None] = None, + ) -> typing.List[SynonymItemSchema]: + params: typing.Dict[str, typing.Union[int, None]] = { + "limit": limit, + "offset": offset, + } + clean_params: typing.Dict[str, int] = { + k: v for k, v in params.items() if v is not None + } + response: typing.List[SynonymItemSchema] = await self.api_call.get( + self._items_path, + as_json=True, + entity_type=typing.List[SynonymItemSchema], + params=clean_params or None, + ) + return response + + async def get_item(self, item_id: str) -> SynonymItemSchema: + response: SynonymItemSchema = await self.api_call.get( + "/".join([self._items_path, item_id]), + as_json=True, + entity_type=SynonymItemSchema, + ) + return response + + async def upsert_item( + self, item_id: str, item: SynonymItemSchema + ) -> SynonymItemSchema: + response: SynonymItemSchema = await self.api_call.put( + "/".join([self._items_path, item_id]), + body=item, + entity_type=SynonymItemSchema, + ) + return response + + async def delete_item(self, item_id: str) -> SynonymItemDeleteSchema: + # API returns {"id": "..."} for delete; openapi defines SynonymItemDeleteResponse with name but for items it's id + response: SynonymItemDeleteSchema = await self.api_call.delete( + "/".join([self._items_path, item_id]), entity_type=SynonymItemDeleteSchema + ) + return response diff --git a/src/typesense/async_synonym_sets.py b/src/typesense/async_synonym_sets.py new file mode 100644 index 0000000..eabdc41 --- /dev/null +++ b/src/typesense/async_synonym_sets.py @@ -0,0 +1,34 @@ +"""Client for Synonym Sets collection operations (async).""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.async_synonym_set import AsyncSynonymSet +from typesense.types.synonym_set import ( + SynonymSetSchema, +) + + +class AsyncSynonymSets: + resource_path: typing.Final[str] = "/synonym_sets" + + def __init__(self, api_call: AsyncApiCall) -> None: + self.api_call = api_call + + async def retrieve(self) -> typing.List[SynonymSetSchema]: + response: typing.List[SynonymSetSchema] = await self.api_call.get( + AsyncSynonymSets.resource_path, + as_json=True, + entity_type=typing.List[SynonymSetSchema], + ) + return response + + def __getitem__(self, synonym_set_name: str) -> AsyncSynonymSet: + from typesense.async_synonym_set import AsyncSynonymSet as PerSet + + return PerSet(self.api_call, synonym_set_name) diff --git a/tests/fixtures/synonym_set_fixtures.py b/tests/fixtures/synonym_set_fixtures.py index 41ad3bb..b898961 100644 --- a/tests/fixtures/synonym_set_fixtures.py +++ b/tests/fixtures/synonym_set_fixtures.py @@ -4,6 +4,9 @@ import requests from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_synonym_set import AsyncSynonymSet +from typesense.async_synonym_sets import AsyncSynonymSets from typesense.synonym_set import SynonymSet from typesense.synonym_sets import SynonymSets @@ -69,3 +72,31 @@ def fake_synonym_sets_fixture(fake_api_call: ApiCall) -> SynonymSets: def fake_synonym_set_fixture(fake_api_call: ApiCall) -> SynonymSet: """Return a SynonymSet object with test values.""" return SynonymSet(fake_api_call, "test-set") + + +@pytest.fixture(scope="function", name="actual_async_synonym_sets") +def actual_async_synonym_sets_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncSynonymSets: + """Return a AsyncSynonymSets object using a real API.""" + return AsyncSynonymSets(actual_async_api_call) + + +@pytest.fixture(scope="function", name="actual_async_synonym_set") +def actual_async_synonym_set_fixture(actual_async_api_call: AsyncApiCall) -> AsyncSynonymSet: + """Return a AsyncSynonymSet object using a real API.""" + return AsyncSynonymSet(actual_async_api_call, "test-set") + + +@pytest.fixture(scope="function", name="fake_async_synonym_sets") +def fake_async_synonym_sets_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncSynonymSets: + """Return a AsyncSynonymSets object with test values.""" + return AsyncSynonymSets(fake_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_synonym_set") +def fake_async_synonym_set_fixture(fake_async_api_call: AsyncApiCall) -> AsyncSynonymSet: + """Return a AsyncSynonymSet object with test values.""" + return AsyncSynonymSet(fake_async_api_call, "test-set") diff --git a/tests/synonym_set_items_test.py b/tests/synonym_set_items_test.py index 0d388a0..aae20a0 100644 --- a/tests/synonym_set_items_test.py +++ b/tests/synonym_set_items_test.py @@ -5,6 +5,7 @@ import pytest from tests.utils.version import is_v30_or_above +from typesense.async_synonym_sets import AsyncSynonymSets from typesense.client import Client from typesense.synonym_sets import SynonymSets from typesense.types.synonym_set import ( @@ -83,3 +84,68 @@ def test_actual_delete_item( response = actual_synonym_sets["test-set"].delete_item("company_synonym") assert response == {"id": "company_synonym"} + + +async def test_actual_list_items_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSet object can list items from Typesense Server.""" + response = await actual_async_synonym_sets["test-set"].list_items() + + assert response == [ + { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + }, + ] + + +async def test_actual_get_item_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSet object can get a specific item from Typesense Server.""" + response = await actual_async_synonym_sets["test-set"].get_item("company_synonym") + + assert response == { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + } + + +async def test_actual_upsert_item_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSet object can upsert an item in Typesense Server.""" + payload: SynonymItemSchema = { + "id": "brand_synonym", + "synonyms": ["brand", "brands", "label"], + } + response = await actual_async_synonym_sets["test-set"].upsert_item( + "brand_synonym", payload + ) + + assert response == { + "id": "brand_synonym", + "synonyms": ["brand", "brands", "label"], + } + + +async def test_actual_delete_item_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSet object can delete an item from Typesense Server.""" + response = await actual_async_synonym_sets["test-set"].delete_item( + "company_synonym" + ) + + assert response == {"id": "company_synonym"} diff --git a/tests/synonym_set_test.py b/tests/synonym_set_test.py index 6e1eaac..ee12871 100644 --- a/tests/synonym_set_test.py +++ b/tests/synonym_set_test.py @@ -7,6 +7,8 @@ from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from tests.utils.version import is_v30_or_above from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_synonym_sets import AsyncSynonymSets from typesense.client import Client from typesense.synonym_set import SynonymSet from typesense.synonym_sets import SynonymSets @@ -69,3 +71,52 @@ def test_actual_delete( response = actual_synonym_sets["test-set"].delete() assert response == {"name": "test-set"} + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncSynonymSet object is initialized correctly.""" + from typesense.async_synonym_set import AsyncSynonymSet + + synset = AsyncSynonymSet(fake_async_api_call, "test-set") + + assert synset.name == "test-set" + assert_match_object(synset.api_call, fake_async_api_call) + assert_object_lists_match( + synset.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + synset.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert synset._endpoint_path == "/synonym_sets/test-set" # noqa: WPS437 + + +async def test_actual_retrieve_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSet object can retrieve a synonym set from Typesense Server.""" + response = await actual_async_synonym_sets["test-set"].retrieve() + + assert response == { + "name": "test-set", + "items": [ + { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + } + ], + } + + +async def test_actual_delete_async( + actual_async_synonym_sets: AsyncSynonymSets, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSet object can delete a synonym set from Typesense Server.""" + response = await actual_async_synonym_sets["test-set"].delete() + + assert response == {"name": "test-set"} diff --git a/tests/synonym_sets_test.py b/tests/synonym_sets_test.py index 26b5859..b02f0fc 100644 --- a/tests/synonym_sets_test.py +++ b/tests/synonym_sets_test.py @@ -1,7 +1,5 @@ """Tests for the SynonymSets class.""" -from __future__ import annotations - import pytest from tests.utils.object_assertions import ( @@ -11,6 +9,8 @@ ) from tests.utils.version import is_v30_or_above from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_synonym_sets import AsyncSynonymSets from typesense.client import Client from typesense.synonym_sets import SynonymSets @@ -85,3 +85,65 @@ def test_actual_retrieve( "name": "test-set", }, ) + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncSynonymSets object is initialized correctly.""" + from typesense.async_synonym_sets import AsyncSynonymSets + + synsets = AsyncSynonymSets(fake_async_api_call) + + assert_match_object(synsets.api_call, fake_async_api_call) + assert_object_lists_match( + synsets.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + synsets.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + +async def test_actual_create_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, +) -> None: + """Test that the AsyncSynonymSets object can create a synonym set on Typesense Server.""" + response = await actual_async_synonym_sets["test-set"].upsert( + { + "items": [ + { + "id": "company_synonym", + "synonyms": ["companies", "corporations", "firms"], + } + ] + }, + ) + + assert response == { + "name": "test-set", + "items": [ + { + "id": "company_synonym", + "root": "", + "synonyms": ["companies", "corporations", "firms"], + } + ], + } + + +async def test_actual_retrieve_async( + actual_async_synonym_sets: AsyncSynonymSets, + delete_all_synonym_sets: None, + create_synonym_set: None, +) -> None: + """Test that the AsyncSynonymSets object can retrieve a synonym set from Typesense Server.""" + response = await actual_async_synonym_sets.retrieve() + + assert isinstance(response, list) + assert_to_contain_object( + response[0], + { + "name": "test-set", + }, + ) From 79845b6d5472a1c3dee939c31256f32fb37748aa Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:18:08 +0200 Subject: [PATCH 27/32] lint: remove annotations imports --- src/typesense/configuration.py | 2 -- tests/analytics_rule_v1_test.py | 1 - tests/analytics_rules_test.py | 1 - tests/collections_test.py | 1 - tests/conversation_model_test.py | 1 - tests/conversations_models_test.py | 1 - tests/curation_set_test.py | 1 - tests/document_test.py | 1 - tests/key_test.py | 1 - tests/keys_test.py | 1 - tests/metrics_test.py | 1 - tests/nl_search_model_test.py | 1 - tests/nl_search_models_test.py | 1 - tests/operations_test.py | 1 - tests/override_test.py | 1 - tests/overrides_test.py | 1 - tests/stopwords_set_test.py | 1 - tests/stopwords_test.py | 1 - tests/synonym_set_items_test.py | 1 - tests/synonym_set_test.py | 1 - tests/synonyms_test.py | 1 - tests/utils/object_assertions.py | 2 -- tests/utils/version.py | 1 - 23 files changed, 25 deletions(-) diff --git a/src/typesense/configuration.py b/src/typesense/configuration.py index 1720233..85159cd 100644 --- a/src/typesense/configuration.py +++ b/src/typesense/configuration.py @@ -14,8 +14,6 @@ - ConfigError: Custom exception for configuration-related errors. """ -from __future__ import annotations - import sys import time diff --git a/tests/analytics_rule_v1_test.py b/tests/analytics_rule_v1_test.py index 9dcf47b..c1147a2 100644 --- a/tests/analytics_rule_v1_test.py +++ b/tests/analytics_rule_v1_test.py @@ -1,6 +1,5 @@ """Tests for the AnalyticsRuleV1 class.""" -from __future__ import annotations import pytest diff --git a/tests/analytics_rules_test.py b/tests/analytics_rules_test.py index f9abe0f..02246f1 100644 --- a/tests/analytics_rules_test.py +++ b/tests/analytics_rules_test.py @@ -1,6 +1,5 @@ """Tests for v30 Analytics Rules endpoints (client.analytics.rules).""" -from __future__ import annotations import pytest diff --git a/tests/collections_test.py b/tests/collections_test.py index 60b4d5d..a11fcb4 100644 --- a/tests/collections_test.py +++ b/tests/collections_test.py @@ -1,6 +1,5 @@ """Tests for the Collections class.""" -from __future__ import annotations import sys diff --git a/tests/conversation_model_test.py b/tests/conversation_model_test.py index 5c2d2dd..e339448 100644 --- a/tests/conversation_model_test.py +++ b/tests/conversation_model_test.py @@ -1,6 +1,5 @@ """Tests for the ConversationModel class.""" -from __future__ import annotations import pytest from dotenv import load_dotenv diff --git a/tests/conversations_models_test.py b/tests/conversations_models_test.py index 7c40498..1b8230c 100644 --- a/tests/conversations_models_test.py +++ b/tests/conversations_models_test.py @@ -1,6 +1,5 @@ """Tests for the ConversationsModels class.""" -from __future__ import annotations import os import sys diff --git a/tests/curation_set_test.py b/tests/curation_set_test.py index bab741e..4a284b0 100644 --- a/tests/curation_set_test.py +++ b/tests/curation_set_test.py @@ -1,6 +1,5 @@ """Tests for the CurationSet class including items APIs.""" -from __future__ import annotations import pytest diff --git a/tests/document_test.py b/tests/document_test.py index fae9d5d..d09e187 100644 --- a/tests/document_test.py +++ b/tests/document_test.py @@ -1,6 +1,5 @@ """Tests for the Document class.""" -from __future__ import annotations import pytest diff --git a/tests/key_test.py b/tests/key_test.py index 9575e44..31ca104 100644 --- a/tests/key_test.py +++ b/tests/key_test.py @@ -1,6 +1,5 @@ """Tests for the Key class.""" -from __future__ import annotations from tests.utils.object_assertions import ( diff --git a/tests/keys_test.py b/tests/keys_test.py index 4786879..89d31d7 100644 --- a/tests/keys_test.py +++ b/tests/keys_test.py @@ -1,6 +1,5 @@ """Tests for the Keys class.""" -from __future__ import annotations import base64 import hashlib diff --git a/tests/metrics_test.py b/tests/metrics_test.py index 8f63043..0bbe57a 100644 --- a/tests/metrics_test.py +++ b/tests/metrics_test.py @@ -1,6 +1,5 @@ """Tests for the Metrics class.""" -from __future__ import annotations from tests.utils.object_assertions import ( assert_match_object, diff --git a/tests/nl_search_model_test.py b/tests/nl_search_model_test.py index 2da60b8..21941f0 100644 --- a/tests/nl_search_model_test.py +++ b/tests/nl_search_model_test.py @@ -1,6 +1,5 @@ """Tests for the NLSearchModel class.""" -from __future__ import annotations import pytest from dotenv import load_dotenv diff --git a/tests/nl_search_models_test.py b/tests/nl_search_models_test.py index e55f038..86e9930 100644 --- a/tests/nl_search_models_test.py +++ b/tests/nl_search_models_test.py @@ -1,6 +1,5 @@ """Tests for the NLSearchModels class.""" -from __future__ import annotations import os import sys diff --git a/tests/operations_test.py b/tests/operations_test.py index 14b28a9..41d2a03 100644 --- a/tests/operations_test.py +++ b/tests/operations_test.py @@ -1,6 +1,5 @@ """Tests for the Operations class.""" -from __future__ import annotations import pytest diff --git a/tests/override_test.py b/tests/override_test.py index c411134..6ac8d9a 100644 --- a/tests/override_test.py +++ b/tests/override_test.py @@ -1,6 +1,5 @@ """Tests for the Override class.""" -from __future__ import annotations import pytest diff --git a/tests/overrides_test.py b/tests/overrides_test.py index 20347f6..114664d 100644 --- a/tests/overrides_test.py +++ b/tests/overrides_test.py @@ -1,6 +1,5 @@ """Tests for the Overrides class.""" -from __future__ import annotations import pytest diff --git a/tests/stopwords_set_test.py b/tests/stopwords_set_test.py index b2d1af2..1bbc581 100644 --- a/tests/stopwords_set_test.py +++ b/tests/stopwords_set_test.py @@ -1,6 +1,5 @@ """Tests for the StopwordsSet class.""" -from __future__ import annotations from tests.utils.object_assertions import assert_match_object, assert_object_lists_match diff --git a/tests/stopwords_test.py b/tests/stopwords_test.py index cce496d..c69f4c0 100644 --- a/tests/stopwords_test.py +++ b/tests/stopwords_test.py @@ -1,6 +1,5 @@ """Tests for the Stopwords class.""" -from __future__ import annotations from tests.utils.object_assertions import ( diff --git a/tests/synonym_set_items_test.py b/tests/synonym_set_items_test.py index aae20a0..c7e9ed0 100644 --- a/tests/synonym_set_items_test.py +++ b/tests/synonym_set_items_test.py @@ -1,6 +1,5 @@ """Tests for SynonymSet item-level APIs.""" -from __future__ import annotations import pytest diff --git a/tests/synonym_set_test.py b/tests/synonym_set_test.py index ee12871..835278f 100644 --- a/tests/synonym_set_test.py +++ b/tests/synonym_set_test.py @@ -1,6 +1,5 @@ """Tests for the SynonymSet class.""" -from __future__ import annotations import pytest diff --git a/tests/synonyms_test.py b/tests/synonyms_test.py index a300c44..9a3da00 100644 --- a/tests/synonyms_test.py +++ b/tests/synonyms_test.py @@ -1,6 +1,5 @@ """Tests for the Synonyms class.""" -from __future__ import annotations import pytest diff --git a/tests/utils/object_assertions.py b/tests/utils/object_assertions.py index a74fb51..ffe95f4 100644 --- a/tests/utils/object_assertions.py +++ b/tests/utils/object_assertions.py @@ -1,7 +1,5 @@ """Utility functions for asserting that objects have the same attribute values.""" -from __future__ import annotations - import difflib import sys diff --git a/tests/utils/version.py b/tests/utils/version.py index 33b9151..f8cced1 100644 --- a/tests/utils/version.py +++ b/tests/utils/version.py @@ -1,4 +1,3 @@ -from __future__ import annotations from typesense.client import Client From c6032349dc2535ec9f5590b40b5ec8e22573c952 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:48:19 +0200 Subject: [PATCH 28/32] chore: bump deps --- uv.lock | 756 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 510 insertions(+), 246 deletions(-) diff --git a/uv.lock b/uv.lock index 16132b1..40d2bb3 100644 --- a/uv.lock +++ b/uv.lock @@ -31,11 +31,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] [[package]] @@ -154,100 +154,260 @@ wheels = [ [[package]] name = "coverage" -version = "7.8.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/6b/7dd06399a5c0b81007e3a6af0395cd60e6a30f959f8d407d3ee04642e896/coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a", size = 211573, upload-time = "2025-05-23T11:37:47.207Z" }, - { url = "https://files.pythonhosted.org/packages/f0/df/2b24090820a0bac1412955fb1a4dade6bc3b8dcef7b899c277ffaf16916d/coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be", size = 212006, upload-time = "2025-05-23T11:37:50.289Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c4/e4e3b998e116625562a872a342419652fa6ca73f464d9faf9f52f1aff427/coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3", size = 241128, upload-time = "2025-05-23T11:37:52.229Z" }, - { url = "https://files.pythonhosted.org/packages/b1/67/b28904afea3e87a895da850ba587439a61699bf4b73d04d0dfd99bbd33b4/coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6", size = 239026, upload-time = "2025-05-23T11:37:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/8c/0f/47bf7c5630d81bc2cd52b9e13043685dbb7c79372a7f5857279cc442b37c/coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622", size = 240172, upload-time = "2025-05-23T11:37:55.711Z" }, - { url = "https://files.pythonhosted.org/packages/ba/38/af3eb9d36d85abc881f5aaecf8209383dbe0fa4cac2d804c55d05c51cb04/coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c", size = 240086, upload-time = "2025-05-23T11:37:57.724Z" }, - { url = "https://files.pythonhosted.org/packages/9e/64/c40c27c2573adeba0fe16faf39a8aa57368a1f2148865d6bb24c67eadb41/coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3", size = 238792, upload-time = "2025-05-23T11:37:59.737Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ab/b7c85146f15457671c1412afca7c25a5696d7625e7158002aa017e2d7e3c/coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404", size = 239096, upload-time = "2025-05-23T11:38:01.693Z" }, - { url = "https://files.pythonhosted.org/packages/d3/50/9446dad1310905fb1dc284d60d4320a5b25d4e3e33f9ea08b8d36e244e23/coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7", size = 214144, upload-time = "2025-05-23T11:38:03.68Z" }, - { url = "https://files.pythonhosted.org/packages/23/ed/792e66ad7b8b0df757db8d47af0c23659cdb5a65ef7ace8b111cacdbee89/coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347", size = 215043, upload-time = "2025-05-23T11:38:05.217Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692, upload-time = "2025-05-23T11:38:08.485Z" }, - { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115, upload-time = "2025-05-23T11:38:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740, upload-time = "2025-05-23T11:38:11.947Z" }, - { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429, upload-time = "2025-05-23T11:38:13.955Z" }, - { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218, upload-time = "2025-05-23T11:38:15.631Z" }, - { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865, upload-time = "2025-05-23T11:38:17.622Z" }, - { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038, upload-time = "2025-05-23T11:38:19.966Z" }, - { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567, upload-time = "2025-05-23T11:38:21.912Z" }, - { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194, upload-time = "2025-05-23T11:38:23.571Z" }, - { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109, upload-time = "2025-05-23T11:38:25.137Z" }, - { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521, upload-time = "2025-05-23T11:38:27.123Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" }, - { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" }, - { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" }, - { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" }, - { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" }, - { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" }, - { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" }, - { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, - { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" }, - { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" }, - { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" }, - { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" }, - { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" }, - { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" }, - { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" }, - { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" }, - { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" }, - { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" }, - { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" }, - { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" }, - { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" }, - { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" }, - { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" }, - { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" }, - { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" }, - { url = "https://files.pythonhosted.org/packages/71/1e/388267ad9c6aa126438acc1ceafede3bb746afa9872e3ec5f0691b7d5efa/coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a", size = 211566, upload-time = "2025-05-23T11:39:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a5/acc03e5cf0bba6357f5e7c676343de40fbf431bb1e115fbebf24b2f7f65e/coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d", size = 211996, upload-time = "2025-05-23T11:39:34.512Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a2/0fc0a9f6b7c24fa4f1d7210d782c38cb0d5e692666c36eaeae9a441b6755/coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca", size = 240741, upload-time = "2025-05-23T11:39:36.252Z" }, - { url = "https://files.pythonhosted.org/packages/e6/da/1c6ba2cf259710eed8916d4fd201dccc6be7380ad2b3b9f63ece3285d809/coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d", size = 238672, upload-time = "2025-05-23T11:39:38.03Z" }, - { url = "https://files.pythonhosted.org/packages/ac/51/c8fae0dc3ca421e6e2509503696f910ff333258db672800c3bdef256265a/coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787", size = 239769, upload-time = "2025-05-23T11:39:40.24Z" }, - { url = "https://files.pythonhosted.org/packages/59/8e/b97042ae92c59f40be0c989df090027377ba53f2d6cef73c9ca7685c26a6/coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7", size = 239555, upload-time = "2025-05-23T11:39:42.3Z" }, - { url = "https://files.pythonhosted.org/packages/47/35/b8893e682d6e96b1db2af5997fc13ef62219426fb17259d6844c693c5e00/coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3", size = 237768, upload-time = "2025-05-23T11:39:44.069Z" }, - { url = "https://files.pythonhosted.org/packages/03/6c/023b0b9a764cb52d6243a4591dcb53c4caf4d7340445113a1f452bb80591/coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7", size = 238757, upload-time = "2025-05-23T11:39:46.195Z" }, - { url = "https://files.pythonhosted.org/packages/03/ed/3af7e4d721bd61a8df7de6de9e8a4271e67f3d9e086454558fd9f48eb4f6/coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a", size = 214166, upload-time = "2025-05-23T11:39:47.934Z" }, - { url = "https://files.pythonhosted.org/packages/9d/30/ee774b626773750dc6128354884652507df3c59d6aa8431526107e595227/coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e", size = 215050, upload-time = "2025-05-23T11:39:50.252Z" }, - { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/08/bdd7ccca14096f7eb01412b87ac11e5d16e4cb54b6e328afc9dee8bdaec1/coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", size = 217979, upload-time = "2025-12-08T13:12:14.505Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/d1302e3416298a28b5663ae1117546a745d9d19fde7e28402b2c5c3e2109/coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", size = 218496, upload-time = "2025-12-08T13:12:16.237Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/d36c354c8b2a320819afcea6bffe72839efd004b98d1d166b90801d49d57/coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", size = 245237, upload-time = "2025-12-08T13:12:17.858Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/be5e85631e0eec547873d8b08dd67a5f6b111ecfe89a86e40b89b0c1c61c/coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", size = 247061, upload-time = "2025-12-08T13:12:19.132Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/a5e8fa0caf05fbd8fa0402470377bff09cc1f026d21c05c71e01295e55ab/coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", size = 248928, upload-time = "2025-12-08T13:12:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/ffb5069b6fd1b95fae482e02f3fecf380d437dd5a39bae09f16d2e2e7e01/coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", size = 245931, upload-time = "2025-12-08T13:12:22.243Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/73e809b882c2858f13e55c0c36e94e09ce07e6165d5644588f9517efe333/coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", size = 246968, upload-time = "2025-12-08T13:12:23.52Z" }, + { url = "https://files.pythonhosted.org/packages/87/08/64ebd9e64b6adb8b4a4662133d706fbaccecab972e0b3ccc23f64e2678ad/coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", size = 244972, upload-time = "2025-12-08T13:12:24.781Z" }, + { url = "https://files.pythonhosted.org/packages/12/97/f4d27c6fe0cb375a5eced4aabcaef22de74766fb80a3d5d2015139e54b22/coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", size = 245241, upload-time = "2025-12-08T13:12:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/0c/94/42f8ae7f633bf4c118bf1038d80472f9dade88961a466f290b81250f7ab7/coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", size = 245847, upload-time = "2025-12-08T13:12:29.337Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/6369ca22b6b6d933f4f4d27765d313d8914cc4cce84f82a16436b1a233db/coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", size = 220573, upload-time = "2025-12-08T13:12:30.905Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/a6a741e519acceaeccc70a7f4cfe5d030efc4b222595f0677e101af6f1f3/coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", size = 221509, upload-time = "2025-12-08T13:12:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, ] [[package]] name = "exceptiongroup" -version = "1.3.0" +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/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +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 = "faker" +version = "37.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "tzdata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/84/e95acaa848b855e15c83331d0401ee5f84b2f60889255c2e055cb4fb6bdf/faker-37.12.0.tar.gz", hash = "sha256:7505e59a7e02fa9010f06c3e1e92f8250d4cfbb30632296140c2d6dbef09b0fa", size = 1935741, upload-time = "2025-10-24T15:19:58.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4", size = 1975461, upload-time = "2025-10-24T15:19:55.739Z" }, ] [[package]] name = "faker" -version = "37.3.0" +version = "38.2.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] dependencies = [ - { name = "tzdata" }, + { name = "tzdata", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/4b/5354912eaff922876323f2d07e21408b10867f3295d5f917748341cb6f53/faker-37.3.0.tar.gz", hash = "sha256:77b79e7a2228d57175133af0bbcdd26dc623df81db390ee52f5104d46c010f2f", size = 1901376, upload-time = "2025-05-14T15:24:18.039Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/27/022d4dbd4c20567b4c294f79a133cc2f05240ea61e0d515ead18c995c249/faker-38.2.0.tar.gz", hash = "sha256:20672803db9c7cb97f9b56c18c54b915b6f1d8991f63d1d673642dc43f5ce7ab", size = 1941469, upload-time = "2025-11-19T16:37:31.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/99/045b2dae19a01b9fbb23b9971bc04f4ef808e7f3a213d08c81067304a210/faker-37.3.0-py3-none-any.whl", hash = "sha256:48c94daa16a432f2d2bc803c7ff602509699fca228d13e97e379cd860a7e216e", size = 1942203, upload-time = "2025-05-14T15:24:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/17/93/00c94d45f55c336434a15f98d906387e87ce28f9918e4444829a8fda432d/faker-38.2.0-py3-none-any.whl", hash = "sha256:35fe4a0a79dee0dc4103a6083ee9224941e7d3594811a50e3969e547b0d2ee65", size = 1980505, upload-time = "2025-11-19T16:37:30.208Z" }, ] [[package]] @@ -289,112 +449,157 @@ wheels = [ [[package]] name = "idna" -version = "3.10" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +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 = "isort" -version = "6.0.1" +version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" }, +] + +[[package]] +name = "isort" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, ] [[package]] name = "librt" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/c3/cdff3c10e2e608490dc0a310ccf11ba777b3943ad4fcead2a2ade98c21e1/librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae", size = 54209, upload-time = "2025-11-29T14:01:56.058Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/84/859df8db21dedab2538ddfbe1d486dda3eb66a98c6ad7ba754a99e25e45e/librt-0.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1", size = 27294, upload-time = "2025-11-29T14:00:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/f7/01/ec3971cf9c4f827f17de6729bdfdbf01a67493147334f4ef8fac68936e3a/librt-0.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf", size = 27635, upload-time = "2025-11-29T14:00:36.496Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f9/3efe201df84dd26388d2e0afa4c4dc668c8e406a3da7b7319152faf835a1/librt-0.6.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c5b31bed2c2f2fa1fcb4815b75f931121ae210dc89a3d607fb1725f5907f1437", size = 81768, upload-time = "2025-11-29T14:00:37.451Z" }, - { url = "https://files.pythonhosted.org/packages/0a/13/f63e60bc219b17f3d8f3d13423cd4972e597b0321c51cac7bfbdd5e1f7b9/librt-0.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f8ed5053ef9fb08d34f1fd80ff093ccbd1f67f147633a84cf4a7d9b09c0f089", size = 85884, upload-time = "2025-11-29T14:00:38.433Z" }, - { url = "https://files.pythonhosted.org/packages/c2/42/0068f14f39a79d1ce8a19d4988dd07371df1d0a7d3395fbdc8a25b1c9437/librt-0.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f0e4bd9bcb0ee34fa3dbedb05570da50b285f49e52c07a241da967840432513", size = 85830, upload-time = "2025-11-29T14:00:39.418Z" }, - { url = "https://files.pythonhosted.org/packages/14/1c/87f5af3a9e6564f09e50c72f82fc3057fd42d1facc8b510a707d0438c4ad/librt-0.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f89c8d20dfa648a3f0a56861946eb00e5b00d6b00eea14bc5532b2fcfa8ef1", size = 88086, upload-time = "2025-11-29T14:00:40.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/e5/22153b98b88a913b5b3f266f12e57df50a2a6960b3f8fcb825b1a0cfe40a/librt-0.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecc2c526547eacd20cb9fbba19a5268611dbc70c346499656d6cf30fae328977", size = 86470, upload-time = "2025-11-29T14:00:41.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/3c/ea1edb587799b1edcc22444e0630fa422e32d7aaa5bfb5115b948acc2d1c/librt-0.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fbedeb9b48614d662822ee514567d2d49a8012037fc7b4cd63f282642c2f4b7d", size = 89079, upload-time = "2025-11-29T14:00:42.882Z" }, - { url = "https://files.pythonhosted.org/packages/73/ad/50bb4ae6b07c9f3ab19653e0830a210533b30eb9a18d515efb5a2b9d0c7c/librt-0.6.3-cp310-cp310-win32.whl", hash = "sha256:0765b0fe0927d189ee14b087cd595ae636bef04992e03fe6dfdaa383866c8a46", size = 19820, upload-time = "2025-11-29T14:00:44.211Z" }, - { url = "https://files.pythonhosted.org/packages/7a/12/7426ee78f3b1dbe11a90619d54cb241ca924ca3c0ff9ade3992178e9b440/librt-0.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c659f9fb8a2f16dc4131b803fa0144c1dadcb3ab24bb7914d01a6da58ae2457", size = 21332, upload-time = "2025-11-29T14:00:45.427Z" }, - { url = "https://files.pythonhosted.org/packages/8b/80/bc60fd16fe24910bf5974fb914778a2e8540cef55385ab2cb04a0dfe42c4/librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08", size = 27285, upload-time = "2025-11-29T14:00:46.626Z" }, - { url = "https://files.pythonhosted.org/packages/88/3c/26335536ed9ba097c79cffcee148393592e55758fe76d99015af3e47a6d0/librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa", size = 27629, upload-time = "2025-11-29T14:00:47.863Z" }, - { url = "https://files.pythonhosted.org/packages/af/fd/2dcedeacfedee5d2eda23e7a49c1c12ce6221b5d58a13555f053203faafc/librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122", size = 82039, upload-time = "2025-11-29T14:00:49.131Z" }, - { url = "https://files.pythonhosted.org/packages/48/ff/6aa11914b83b0dc2d489f7636942a8e3322650d0dba840db9a1b455f3caa/librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f", size = 86560, upload-time = "2025-11-29T14:00:50.403Z" }, - { url = "https://files.pythonhosted.org/packages/76/a1/d25af61958c2c7eb978164aeba0350719f615179ba3f428b682b9a5fdace/librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040", size = 86494, upload-time = "2025-11-29T14:00:51.383Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4b/40e75d3b258c801908e64b39788f9491635f9554f8717430a491385bd6f2/librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523", size = 88914, upload-time = "2025-11-29T14:00:52.688Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/0070c81aba8a169224301c75fb5fb6c3c25ca67e6ced086584fc130d5a67/librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f", size = 86944, upload-time = "2025-11-29T14:00:53.768Z" }, - { url = "https://files.pythonhosted.org/packages/a6/94/809f38887941b7726692e0b5a083dbdc87dbb8cf893e3b286550c5f0b129/librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47", size = 89852, upload-time = "2025-11-29T14:00:54.71Z" }, - { url = "https://files.pythonhosted.org/packages/58/a3/b0e5b1cda675b91f1111d8ba941da455d8bfaa22f4d2d8963ba96ccb5b12/librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7", size = 19948, upload-time = "2025-11-29T14:00:55.989Z" }, - { url = "https://files.pythonhosted.org/packages/cc/73/70011c2b37e3be3ece3affd3abc8ebe5cda482b03fd6b3397906321a901e/librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8", size = 21406, upload-time = "2025-11-29T14:00:56.874Z" }, - { url = "https://files.pythonhosted.org/packages/91/ee/119aa759290af6ca0729edf513ca390c1afbeae60f3ecae9b9d56f25a8a9/librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45", size = 20875, upload-time = "2025-11-29T14:00:57.752Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2c/b59249c566f98fe90e178baf59e83f628d6c38fb8bc78319301fccda0b5e/librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176", size = 27841, upload-time = "2025-11-29T14:00:58.925Z" }, - { url = "https://files.pythonhosted.org/packages/40/e8/9db01cafcd1a2872b76114c858f81cc29ce7ad606bc102020d6dabf470fb/librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057", size = 27844, upload-time = "2025-11-29T14:01:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/59/4d/da449d3a7d83cc853af539dee42adc37b755d7eea4ad3880bacfd84b651d/librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610", size = 84091, upload-time = "2025-11-29T14:01:01.118Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6c/f90306906fb6cc6eaf4725870f0347115de05431e1f96d35114392d31fda/librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c", size = 88239, upload-time = "2025-11-29T14:01:02.11Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ae/473ce7b423cfac2cb503851a89d9d2195bf615f534d5912bf86feeebbee7/librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9", size = 88815, upload-time = "2025-11-29T14:01:03.114Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6d/934df738c87fb9617cabefe4891eece585a06abe6def25b4bca3b174429d/librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c", size = 90598, upload-time = "2025-11-29T14:01:04.071Z" }, - { url = "https://files.pythonhosted.org/packages/72/89/eeaa124f5e0f431c2b39119550378ae817a4b1a3c93fd7122f0639336fff/librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c", size = 88603, upload-time = "2025-11-29T14:01:05.02Z" }, - { url = "https://files.pythonhosted.org/packages/4d/ed/c60b3c1cfc27d709bc0288af428ce58543fcb5053cf3eadbc773c24257f5/librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf", size = 92112, upload-time = "2025-11-29T14:01:06.304Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ab/f56169be5f716ef4ab0277be70bcb1874b4effc262e655d85b505af4884d/librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d", size = 20127, upload-time = "2025-11-29T14:01:07.283Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8d/222750ce82bf95125529eaab585ac7e2829df252f3cfc05d68792fb1dd2c/librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01", size = 21545, upload-time = "2025-11-29T14:01:08.184Z" }, - { url = "https://files.pythonhosted.org/packages/72/c9/f731ddcfb72f446a92a8674c6b8e1e2242773cce43a04f41549bd8b958ff/librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55", size = 20946, upload-time = "2025-11-29T14:01:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/dd/aa/3055dd440f8b8b3b7e8624539a0749dd8e1913e978993bcca9ce7e306231/librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3", size = 27874, upload-time = "2025-11-29T14:01:10.615Z" }, - { url = "https://files.pythonhosted.org/packages/ef/93/226d7dd455eaa4c26712b5ccb2dfcca12831baa7f898c8ffd3a831e29fda/librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5", size = 27852, upload-time = "2025-11-29T14:01:11.535Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8b/db9d51191aef4e4cc06285250affe0bb0ad8b2ed815f7ca77951655e6f02/librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42", size = 84264, upload-time = "2025-11-29T14:01:12.461Z" }, - { url = "https://files.pythonhosted.org/packages/8d/53/297c96bda3b5a73bdaf748f1e3ae757edd29a0a41a956b9c10379f193417/librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70", size = 88432, upload-time = "2025-11-29T14:01:13.405Z" }, - { url = "https://files.pythonhosted.org/packages/54/3a/c005516071123278e340f22de72fa53d51e259d49215295c212da16c4dc2/librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522", size = 89014, upload-time = "2025-11-29T14:01:14.373Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9b/ea715f818d926d17b94c80a12d81a79e95c44f52848e61e8ca1ff29bb9a9/librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30", size = 90807, upload-time = "2025-11-29T14:01:15.377Z" }, - { url = "https://files.pythonhosted.org/packages/f0/fc/4e2e4c87e002fa60917a8e474fd13c4bac9a759df82be3778573bb1ab954/librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917", size = 88890, upload-time = "2025-11-29T14:01:16.633Z" }, - { url = "https://files.pythonhosted.org/packages/70/7f/c7428734fbdfd4db3d5b9237fc3a857880b2ace66492836f6529fef25d92/librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530", size = 92300, upload-time = "2025-11-29T14:01:17.658Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0c/738c4824fdfe74dc0f95d5e90ef9e759d4ecf7fd5ba964d54a7703322251/librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3", size = 20159, upload-time = "2025-11-29T14:01:18.61Z" }, - { url = "https://files.pythonhosted.org/packages/f2/95/93d0e61bc617306ecf4c54636b5cbde4947d872563565c4abdd9d07a39d3/librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88", size = 21484, upload-time = "2025-11-29T14:01:19.506Z" }, - { url = "https://files.pythonhosted.org/packages/10/23/abd7ace79ab54d1dbee265f13529266f686a7ce2d21ab59a992f989009b6/librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9", size = 20935, upload-time = "2025-11-29T14:01:20.415Z" }, - { url = "https://files.pythonhosted.org/packages/83/14/c06cb31152182798ed98be73f54932ab984894f5a8fccf9b73130897a938/librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a", size = 27566, upload-time = "2025-11-29T14:01:21.609Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/ce83ca7b057b06150519152f53a0b302d7c33c8692ce2f01f669b5a819d9/librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd", size = 27753, upload-time = "2025-11-29T14:01:22.558Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ec/739a885ef0a2839b6c25f1b01c99149d2cb6a34e933ffc8c051fcd22012e/librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b", size = 83178, upload-time = "2025-11-29T14:01:23.555Z" }, - { url = "https://files.pythonhosted.org/packages/db/bd/dc18bb1489d48c0911b9f4d72eae2d304ea264e215ba80f1e6ba4a9fc41d/librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc", size = 87266, upload-time = "2025-11-29T14:01:24.532Z" }, - { url = "https://files.pythonhosted.org/packages/94/f3/d0c5431b39eef15e48088b2d739ad84b17c2f1a22c0345c6d4c4a42b135e/librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3", size = 87623, upload-time = "2025-11-29T14:01:25.798Z" }, - { url = "https://files.pythonhosted.org/packages/3b/15/9a52e90834e4bd6ee16cdbaf551cb32227cbaad27398391a189c489318bc/librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751", size = 89436, upload-time = "2025-11-29T14:01:26.769Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8a/a7e78e46e8486e023c50f21758930ef4793999115229afd65de69e94c9cc/librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8", size = 87540, upload-time = "2025-11-29T14:01:27.756Z" }, - { url = "https://files.pythonhosted.org/packages/49/01/93799044a1cccac31f1074b07c583e181829d240539657e7f305ae63ae2a/librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e", size = 90597, upload-time = "2025-11-29T14:01:29.35Z" }, - { url = "https://files.pythonhosted.org/packages/a7/29/00c7f58b8f8eb1bad6529ffb6c9cdcc0890a27dac59ecda04f817ead5277/librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf", size = 18955, upload-time = "2025-11-29T14:01:30.325Z" }, - { url = "https://files.pythonhosted.org/packages/d7/13/2739e6e197a9f751375a37908a6a5b0bff637b81338497a1bcb5817394da/librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2", size = 20263, upload-time = "2025-11-29T14:01:31.287Z" }, - { url = "https://files.pythonhosted.org/packages/e1/73/393868fc2158705ea003114a24e73bb10b03bda31e9ad7b5c5ec6575338b/librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78", size = 19575, upload-time = "2025-11-29T14:01:32.229Z" }, - { url = "https://files.pythonhosted.org/packages/48/6d/3c8ff3dec21bf804a205286dd63fd28dcdbe00b8dd7eb7ccf2e21a40a0b0/librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179", size = 28732, upload-time = "2025-11-29T14:01:33.165Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/e214b8b4aa34ed3d3f1040719c06c4d22472c40c5ef81a922d5af7876eb4/librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7", size = 29065, upload-time = "2025-11-29T14:01:34.088Z" }, - { url = "https://files.pythonhosted.org/packages/ab/90/ef61ed51f0a7770cc703422d907a757bbd8811ce820c333d3db2fd13542a/librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916", size = 93703, upload-time = "2025-11-29T14:01:35.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ae/c30bb119c35962cbe9a908a71da99c168056fc3f6e9bbcbc157d0b724d89/librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8", size = 98890, upload-time = "2025-11-29T14:01:36.031Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/47a4a78d252d36f072b79d592df10600d379a895c3880c8cbd2ac699f0ad/librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc", size = 98255, upload-time = "2025-11-29T14:01:37.058Z" }, - { url = "https://files.pythonhosted.org/packages/e5/28/779b5cc3cd9987683884eb5f5672e3251676bebaaae6b7da1cf366eb1da1/librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07", size = 100769, upload-time = "2025-11-29T14:01:38.413Z" }, - { url = "https://files.pythonhosted.org/packages/28/d7/771755e57c375cb9d25a4e106f570607fd856e2cb91b02418db1db954796/librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f", size = 98580, upload-time = "2025-11-29T14:01:39.459Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ec/8b157eb8fbc066339a2f34b0aceb2028097d0ed6150a52e23284a311eafe/librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0", size = 101706, upload-time = "2025-11-29T14:01:40.474Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/4aaead9a06c795a318282aebf7d3e3e578fa889ff396e1b640c3be4c7806/librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a", size = 19465, upload-time = "2025-11-29T14:01:41.77Z" }, - { url = "https://files.pythonhosted.org/packages/3a/61/b7e6a02746c1731670c19ba07d86da90b1ae45d29e405c0b5615abf97cde/librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf", size = 21042, upload-time = "2025-11-29T14:01:42.865Z" }, - { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246, upload-time = "2025-11-29T14:01:44.13Z" }, - { url = "https://files.pythonhosted.org/packages/d0/85/63b34f02f56b86574bb6d1ed29415346332a1edea2cd12b29fb0863456cb/librt-0.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09262cb2445b6f15d09141af20b95bb7030c6f13b00e876ad8fdd1a9045d6aa5", size = 27438, upload-time = "2025-11-29T14:01:45.101Z" }, - { url = "https://files.pythonhosted.org/packages/57/cc/d2952a97b5e19c0a88f04b161d1c4b8336ad093a8fecd49801258b3cc816/librt-0.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57705e8eec76c5b77130d729c0f70190a9773366c555c5457c51eace80afd873", size = 27783, upload-time = "2025-11-29T14:01:46.398Z" }, - { url = "https://files.pythonhosted.org/packages/09/38/007124092f9345a273b27b070e894afa66a737b576f1f7c37354dd4ffe24/librt-0.6.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3ac2a7835434b31def8ed5355dd9b895bbf41642d61967522646d1d8b9681106", size = 81403, upload-time = "2025-11-29T14:01:47.659Z" }, - { url = "https://files.pythonhosted.org/packages/89/ca/079722b2b518bc6c38e3ba7ab07f2f0e5c6d4905d7f34ee138f862a69853/librt-0.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71f0a5918aebbea1e7db2179a8fe87e8a8732340d9e8b8107401fb407eda446e", size = 85678, upload-time = "2025-11-29T14:01:48.638Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7a/dcba5bd2f71d3815ea3886929f218c259665b9f2f7115163eed2ccb50599/librt-0.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa346e202e6e1ebc01fe1c69509cffe486425884b96cb9ce155c99da1ecbe0e9", size = 85655, upload-time = "2025-11-29T14:01:49.894Z" }, - { url = "https://files.pythonhosted.org/packages/7a/47/5bfa3816b58e6105d1ea1a165af73ae6560b4acb80ce3304793ac9a36ec1/librt-0.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92267f865c7bbd12327a0d394666948b9bf4b51308b52947c0cc453bfa812f5d", size = 88093, upload-time = "2025-11-29T14:01:50.902Z" }, - { url = "https://files.pythonhosted.org/packages/76/33/ac5c01cfc68b208423a00451640fad6744f974a9c9e8a62d89e9a5e47159/librt-0.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:86605d5bac340beb030cbc35859325982a79047ebdfba1e553719c7126a2389d", size = 86386, upload-time = "2025-11-29T14:01:51.933Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4d/9460e0b6d53cabeb78016e7e4e7d70dcd11fe90ef37c704856bd8a2ff533/librt-0.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98e4bbecbef8d2a60ecf731d735602feee5ac0b32117dbbc765e28b054bac912", size = 89064, upload-time = "2025-11-29T14:01:53.249Z" }, - { url = "https://files.pythonhosted.org/packages/20/f3/cb8410cbd34718fe7e11b006bacc8093d3f758c7d771c95613caebe7a9fc/librt-0.6.3-cp39-cp39-win32.whl", hash = "sha256:3caa0634c02d5ff0b2ae4a28052e0d8c5f20d497623dc13f629bd4a9e2a6efad", size = 19867, upload-time = "2025-11-29T14:01:54.201Z" }, - { url = "https://files.pythonhosted.org/packages/14/7e/521c046b3bc9316c408d159bc4f2c4be607280b3646416b953bdd4efda6f/librt-0.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:b47395091e7e0ece1e6ebac9b98bf0c9084d1e3d3b2739aa566be7e56e3f7bf2", size = 21408, upload-time = "2025-11-29T14:01:55.126Z" }, +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/d9/6f3d3fcf5e5543ed8a60cc70fa7d50508ed60b8a10e9af6d2058159ab54e/librt-0.7.3.tar.gz", hash = "sha256:3ec50cf65235ff5c02c5b747748d9222e564ad48597122a361269dd3aa808798", size = 144549, upload-time = "2025-12-06T19:04:45.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/66/79a14e672256ef58144a24eb49adb338ec02de67ff4b45320af6504682ab/librt-0.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2682162855a708e3270eba4b92026b93f8257c3e65278b456c77631faf0f4f7a", size = 54707, upload-time = "2025-12-06T19:03:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/58/fa/b709c65a9d5eab85f7bcfe0414504d9775aaad6e78727a0327e175474caa/librt-0.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:440c788f707c061d237c1e83edf6164ff19f5c0f823a3bf054e88804ebf971ec", size = 56670, upload-time = "2025-12-06T19:03:12.107Z" }, + { url = "https://files.pythonhosted.org/packages/3a/56/0685a0772ec89ddad4c00e6b584603274c3d818f9a68e2c43c4eb7b39ee9/librt-0.7.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399938edbd3d78339f797d685142dd8a623dfaded023cf451033c85955e4838a", size = 161045, upload-time = "2025-12-06T19:03:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d9/863ada0c5ce48aefb89df1555e392b2209fcb6daee4c153c031339b9a89b/librt-0.7.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1975eda520957c6e0eb52d12968dd3609ffb7eef05d4223d097893d6daf1d8a7", size = 169532, upload-time = "2025-12-06T19:03:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/a0/71da6c8724fd16c31749905ef1c9e11de206d9301b5be984bf2682b4efb3/librt-0.7.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9da128d0edf990cf0d2ca011b02cd6f639e79286774bd5b0351245cbb5a6e51", size = 183277, upload-time = "2025-12-06T19:03:16.446Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/9c97bf2f8338ba1914de233ea312bba2bbd7c59f43f807b3e119796bab18/librt-0.7.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19acfde38cb532a560b98f473adc741c941b7a9bc90f7294bc273d08becb58b", size = 179045, upload-time = "2025-12-06T19:03:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/ceea067f489e904cb4ddcca3c9b06ba20229bc3fa7458711e24a5811f162/librt-0.7.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7b4f57f7a0c65821c5441d98c47ff7c01d359b1e12328219709bdd97fdd37f90", size = 173521, upload-time = "2025-12-06T19:03:19.17Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/6cb18f5da9c89ed087417abb0127a445a50ad4eaf1282ba5b52588187f47/librt-0.7.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:256793988bff98040de23c57cf36e1f4c2f2dc3dcd17537cdac031d3b681db71", size = 193592, upload-time = "2025-12-06T19:03:20.637Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3c/fcef208746584e7c78584b7aedc617130c4a4742cb8273361bbda8b183b5/librt-0.7.3-cp310-cp310-win32.whl", hash = "sha256:fcb72249ac4ea81a7baefcbff74df7029c3cb1cf01a711113fa052d563639c9c", size = 47201, upload-time = "2025-12-06T19:03:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bf/d8a6c35d1b2b789a4df9b3ddb1c8f535ea373fde2089698965a8f0d62138/librt-0.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:4887c29cadbdc50640179e3861c276325ff2986791e6044f73136e6e798ff806", size = 54371, upload-time = "2025-12-06T19:03:23.231Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/f6391f5c6f158d31ed9af6bd1b1bcd3ffafdea1d816bc4219d0d90175a7f/librt-0.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:687403cced6a29590e6be6964463835315905221d797bc5c934a98750fe1a9af", size = 54711, upload-time = "2025-12-06T19:03:24.6Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1b/53c208188c178987c081560a0fcf36f5ca500d5e21769596c845ef2f40d4/librt-0.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24d70810f6e2ea853ff79338001533716b373cc0f63e2a0be5bc96129edb5fb5", size = 56664, upload-time = "2025-12-06T19:03:25.969Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/d9da832b9a1e5f8366e8a044ec80217945385b26cb89fd6f94bfdc7d80b0/librt-0.7.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf8c7735fbfc0754111f00edda35cf9e98a8d478de6c47b04eaa9cef4300eaa7", size = 161701, upload-time = "2025-12-06T19:03:27.035Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/1e0a7aba15e78529dd21f233076b876ee58c8b8711b1793315bdd3b263b0/librt-0.7.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32d43610dff472eab939f4d7fbdd240d1667794192690433672ae22d7af8445", size = 171040, upload-time = "2025-12-06T19:03:28.482Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/3cfa325c1c2bc25775ec6ec1718cfbec9cff4ac767d37d2d3a2d1cc6f02c/librt-0.7.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:adeaa886d607fb02563c1f625cf2ee58778a2567c0c109378da8f17ec3076ad7", size = 184720, upload-time = "2025-12-06T19:03:29.599Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/e4553433d7ac47f4c75d0a7e59b13aee0e08e88ceadbee356527a9629b0a/librt-0.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:572a24fc5958c61431da456a0ef1eeea6b4989d81eeb18b8e5f1f3077592200b", size = 180731, upload-time = "2025-12-06T19:03:31.201Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/51cd73006232981a3106d4081fbaa584ac4e27b49bc02266468d3919db03/librt-0.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6488e69d408b492e08bfb68f20c4a899a354b4386a446ecd490baff8d0862720", size = 174565, upload-time = "2025-12-06T19:03:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/42/54/0578a78b587e5aa22486af34239a052c6366835b55fc307bc64380229e3f/librt-0.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed028fc3d41adda916320712838aec289956c89b4f0a361ceadf83a53b4c047a", size = 195247, upload-time = "2025-12-06T19:03:34.434Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0a/ee747cd999753dd9447e50b98fc36ee433b6c841a42dbf6d47b64b32a56e/librt-0.7.3-cp311-cp311-win32.whl", hash = "sha256:2cf9d73499486ce39eebbff5f42452518cc1f88d8b7ea4a711ab32962b176ee2", size = 47514, upload-time = "2025-12-06T19:03:35.959Z" }, + { url = "https://files.pythonhosted.org/packages/ec/af/8b13845178dec488e752878f8e290f8f89e7e34ae1528b70277aa1a6dd1e/librt-0.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:35f1609e3484a649bb80431310ddbec81114cd86648f1d9482bc72a3b86ded2e", size = 54695, upload-time = "2025-12-06T19:03:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/ae59578501b1a25850266778f59279f4f3e726acc5c44255bfcb07b4bc57/librt-0.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:550fdbfbf5bba6a2960b27376ca76d6aaa2bd4b1a06c4255edd8520c306fcfc0", size = 48142, upload-time = "2025-12-06T19:03:38.263Z" }, + { url = "https://files.pythonhosted.org/packages/29/90/ed8595fa4e35b6020317b5ea8d226a782dcbac7a997c19ae89fb07a41c66/librt-0.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fa9ac2e49a6bee56e47573a6786cb635e128a7b12a0dc7851090037c0d397a3", size = 55687, upload-time = "2025-12-06T19:03:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f6/6a20702a07b41006cb001a759440cb6b5362530920978f64a2b2ae2bf729/librt-0.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e980cf1ed1a2420a6424e2ed884629cdead291686f1048810a817de07b5eb18", size = 57127, upload-time = "2025-12-06T19:03:40.3Z" }, + { url = "https://files.pythonhosted.org/packages/79/f3/b0c4703d5ffe9359b67bb2ccb86c42d4e930a363cfc72262ac3ba53cff3e/librt-0.7.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e094e445c37c57e9ec612847812c301840239d34ccc5d153a982fa9814478c60", size = 165336, upload-time = "2025-12-06T19:03:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/02/69/3ba05b73ab29ccbe003856232cea4049769be5942d799e628d1470ed1694/librt-0.7.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aca73d70c3f553552ba9133d4a09e767dcfeee352d8d8d3eb3f77e38a3beb3ed", size = 174237, upload-time = "2025-12-06T19:03:42.44Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/d7c2671e7bf6c285ef408aa435e9cd3fdc06fd994601e1f2b242df12034f/librt-0.7.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c634a0a6db395fdaba0361aa78395597ee72c3aad651b9a307a3a7eaf5efd67e", size = 189017, upload-time = "2025-12-06T19:03:44.01Z" }, + { url = "https://files.pythonhosted.org/packages/f4/94/d13f57193148004592b618555f296b41d2d79b1dc814ff8b3273a0bf1546/librt-0.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a59a69deeb458c858b8fea6acf9e2acd5d755d76cd81a655256bc65c20dfff5b", size = 183983, upload-time = "2025-12-06T19:03:45.834Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/b612a9944ebd39fa143c7e2e2d33f2cb790205e025ddd903fb509a3a3bb3/librt-0.7.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d91e60ac44bbe3a77a67af4a4c13114cbe9f6d540337ce22f2c9eaf7454ca71f", size = 177602, upload-time = "2025-12-06T19:03:46.944Z" }, + { url = "https://files.pythonhosted.org/packages/1f/48/77bc05c4cc232efae6c5592c0095034390992edbd5bae8d6cf1263bb7157/librt-0.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:703456146dc2bf430f7832fd1341adac5c893ec3c1430194fdcefba00012555c", size = 199282, upload-time = "2025-12-06T19:03:48.069Z" }, + { url = "https://files.pythonhosted.org/packages/12/aa/05916ccd864227db1ffec2a303ae34f385c6b22d4e7ce9f07054dbcf083c/librt-0.7.3-cp312-cp312-win32.whl", hash = "sha256:b7c1239b64b70be7759554ad1a86288220bbb04d68518b527783c4ad3fb4f80b", size = 47879, upload-time = "2025-12-06T19:03:49.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/92/7f41c42d31ea818b3c4b9cc1562e9714bac3c676dd18f6d5dd3d0f2aa179/librt-0.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef59c938f72bdbc6ab52dc50f81d0637fde0f194b02d636987cea2ab30f8f55a", size = 54972, upload-time = "2025-12-06T19:03:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/dc/53582bbfb422311afcbc92adb75711f04e989cec052f08ec0152fbc36c9c/librt-0.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:ff21c554304e8226bf80c3a7754be27c6c3549a9fec563a03c06ee8f494da8fc", size = 48338, upload-time = "2025-12-06T19:03:51.431Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/e0ce1837dfb452427db556e6d4c5301ba3b22fe8de318379fbd0593759b9/librt-0.7.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56f2a47beda8409061bc1c865bef2d4bd9ff9255219402c0817e68ab5ad89aed", size = 55742, upload-time = "2025-12-06T19:03:52.459Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/3564262301e507e1d5cf31c7d84cb12addf0d35e05ba53312494a2eba9a4/librt-0.7.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14569ac5dd38cfccf0a14597a88038fb16811a6fede25c67b79c6d50fc2c8fdc", size = 57163, upload-time = "2025-12-06T19:03:53.516Z" }, + { url = "https://files.pythonhosted.org/packages/be/ac/245e72b7e443d24a562f6047563c7f59833384053073ef9410476f68505b/librt-0.7.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6038ccbd5968325a5d6fd393cf6e00b622a8de545f0994b89dd0f748dcf3e19e", size = 165840, upload-time = "2025-12-06T19:03:54.918Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/587e4491f40adba066ba39a450c66bad794c8d92094f936a201bfc7c2b5f/librt-0.7.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d39079379a9a28e74f4d57dc6357fa310a1977b51ff12239d7271ec7e71d67f5", size = 174827, upload-time = "2025-12-06T19:03:56.082Z" }, + { url = "https://files.pythonhosted.org/packages/78/21/5b8c60ea208bc83dd00421022a3874330685d7e856404128dc3728d5d1af/librt-0.7.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8837d5a52a2d7aa9f4c3220a8484013aed1d8ad75240d9a75ede63709ef89055", size = 189612, upload-time = "2025-12-06T19:03:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/8b819169ef696421fb81cd04c6cdf225f6e96f197366001e9d45180d7e9e/librt-0.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:399bbd7bcc1633c3e356ae274a1deb8781c7bf84d9c7962cc1ae0c6e87837292", size = 184584, upload-time = "2025-12-06T19:03:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/af9d225a9395b77bd7678362cb055d0b8139c2018c37665de110ca388022/librt-0.7.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8d8cf653e798ee4c4e654062b633db36984a1572f68c3aa25e364a0ddfbbb910", size = 178269, upload-time = "2025-12-06T19:03:59.769Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d8/7b4fa1683b772966749d5683aa3fd605813defffe157833a8fa69cc89207/librt-0.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f03484b54bf4ae80ab2e504a8d99d20d551bfe64a7ec91e218010b467d77093", size = 199852, upload-time = "2025-12-06T19:04:00.901Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/4598413aece46ca38d9260ef6c51534bd5f34b5c21474fcf210ce3a02123/librt-0.7.3-cp313-cp313-win32.whl", hash = "sha256:44b3689b040df57f492e02cd4f0bacd1b42c5400e4b8048160c9d5e866de8abe", size = 47936, upload-time = "2025-12-06T19:04:02.054Z" }, + { url = "https://files.pythonhosted.org/packages/af/80/ac0e92d5ef8c6791b3e2c62373863827a279265e0935acdf807901353b0e/librt-0.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:6b407c23f16ccc36614c136251d6b32bf30de7a57f8e782378f1107be008ddb0", size = 54965, upload-time = "2025-12-06T19:04:03.224Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/042f823fcbff25c1449bb4203a29919891ca74141b68d3a5f6612c4ce283/librt-0.7.3-cp313-cp313-win_arm64.whl", hash = "sha256:abfc57cab3c53c4546aee31859ef06753bfc136c9d208129bad23e2eca39155a", size = 48350, upload-time = "2025-12-06T19:04:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ae/c6ecc7bb97134a71b5241e8855d39964c0e5f4d96558f0d60593892806d2/librt-0.7.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:120dd21d46ff875e849f1aae19346223cf15656be489242fe884036b23d39e93", size = 55175, upload-time = "2025-12-06T19:04:05.308Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/2cc0cb0ab787b39aa5c7645cd792433c875982bdf12dccca558b89624594/librt-0.7.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1617bea5ab31266e152871208502ee943cb349c224846928a1173c864261375e", size = 56881, upload-time = "2025-12-06T19:04:06.674Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/397417a386190b70f5bf26fcedbaa1515f19dce33366e2684c6b7ee83086/librt-0.7.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93b2a1f325fefa1482516ced160c8c7b4b8d53226763fa6c93d151fa25164207", size = 163710, upload-time = "2025-12-06T19:04:08.437Z" }, + { url = "https://files.pythonhosted.org/packages/c9/37/7338f85b80e8a17525d941211451199845093ca242b32efbf01df8531e72/librt-0.7.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d4801db8354436fd3936531e7f0e4feb411f62433a6b6cb32bb416e20b529f", size = 172471, upload-time = "2025-12-06T19:04:10.124Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/741704edabbfae2c852fedc1b40d9ed5a783c70ed3ed8e4fe98f84b25d13/librt-0.7.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11ad45122bbed42cfc8b0597450660126ef28fd2d9ae1a219bc5af8406f95678", size = 186804, upload-time = "2025-12-06T19:04:11.586Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d1/0a82129d6ba242f3be9af34815be089f35051bc79619f5c27d2c449ecef6/librt-0.7.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4e7bff1d76dd2b46443078519dc75df1b5e01562345f0bb740cea5266d8218", size = 181817, upload-time = "2025-12-06T19:04:12.802Z" }, + { url = "https://files.pythonhosted.org/packages/4f/32/704f80bcf9979c68d4357c46f2af788fbf9d5edda9e7de5786ed2255e911/librt-0.7.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d86f94743a11873317094326456b23f8a5788bad9161fd2f0e52088c33564620", size = 175602, upload-time = "2025-12-06T19:04:14.004Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6d/4355cfa0fae0c062ba72f541d13db5bc575770125a7ad3d4f46f4109d305/librt-0.7.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:754a0d09997095ad764ccef050dd5bf26cbf457aab9effcba5890dad081d879e", size = 196497, upload-time = "2025-12-06T19:04:15.487Z" }, + { url = "https://files.pythonhosted.org/packages/2e/eb/ac6d8517d44209e5a712fde46f26d0055e3e8969f24d715f70bd36056230/librt-0.7.3-cp314-cp314-win32.whl", hash = "sha256:fbd7351d43b80d9c64c3cfcb50008f786cc82cba0450e8599fdd64f264320bd3", size = 44678, upload-time = "2025-12-06T19:04:16.688Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/238f026d141faf9958da588c761a0812a1a21c98cc54a76f3608454e4e59/librt-0.7.3-cp314-cp314-win_amd64.whl", hash = "sha256:d376a35c6561e81d2590506804b428fc1075fcc6298fc5bb49b771534c0ba010", size = 51689, upload-time = "2025-12-06T19:04:17.726Z" }, + { url = "https://files.pythonhosted.org/packages/52/44/43f462ad9dcf9ed7d3172fe2e30d77b980956250bd90e9889a9cca93df2a/librt-0.7.3-cp314-cp314-win_arm64.whl", hash = "sha256:cbdb3f337c88b43c3b49ca377731912c101178be91cb5071aac48faa898e6f8e", size = 44662, upload-time = "2025-12-06T19:04:18.771Z" }, + { url = "https://files.pythonhosted.org/packages/1d/35/fed6348915f96b7323241de97f26e2af481e95183b34991df12fd5ce31b1/librt-0.7.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9f0e0927efe87cd42ad600628e595a1a0aa1c64f6d0b55f7e6059079a428641a", size = 57347, upload-time = "2025-12-06T19:04:19.812Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f2/045383ccc83e3fea4fba1b761796584bc26817b6b2efb6b8a6731431d16f/librt-0.7.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:020c6db391268bcc8ce75105cb572df8cb659a43fd347366aaa407c366e5117a", size = 59223, upload-time = "2025-12-06T19:04:20.862Z" }, + { url = "https://files.pythonhosted.org/packages/77/3f/c081f8455ab1d7f4a10dbe58463ff97119272ff32494f21839c3b9029c2c/librt-0.7.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7af7785f5edd1f418da09a8cdb9ec84b0213e23d597413e06525340bcce1ea4f", size = 183861, upload-time = "2025-12-06T19:04:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f5/73c5093c22c31fbeaebc25168837f05ebfd8bf26ce00855ef97a5308f36f/librt-0.7.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ccadf260bb46a61b9c7e89e2218f6efea9f3eeaaab4e3d1f58571890e54858e", size = 194594, upload-time = "2025-12-06T19:04:23.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/b8/d5f17d4afe16612a4a94abfded94c16c5a033f183074fb130dfe56fc1a42/librt-0.7.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9883b2d819ce83f87ba82a746c81d14ada78784db431e57cc9719179847376e", size = 206759, upload-time = "2025-12-06T19:04:24.328Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/021765c1be85ee23ffd5b5b968bb4cba7526a4db2a0fc27dcafbdfc32da7/librt-0.7.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:59cb0470612d21fa1efddfa0dd710756b50d9c7fb6c1236bbf8ef8529331dc70", size = 203210, upload-time = "2025-12-06T19:04:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/9923656e42da4fd18c594bd08cf6d7e152d4158f8b808e210d967f0dcceb/librt-0.7.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1fe603877e1865b5fd047a5e40379509a4a60204aa7aa0f72b16f7a41c3f0712", size = 196708, upload-time = "2025-12-06T19:04:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0b/0708b886ac760e64d6fbe7e16024e4be3ad1a3629d19489a97e9cf4c3431/librt-0.7.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5460d99ed30f043595bbdc888f542bad2caeb6226b01c33cda3ae444e8f82d42", size = 217212, upload-time = "2025-12-06T19:04:27.892Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7f/12a73ff17bca4351e73d585dd9ebf46723c4a8622c4af7fe11a2e2d011ff/librt-0.7.3-cp314-cp314t-win32.whl", hash = "sha256:d09f677693328503c9e492e33e9601464297c01f9ebd966ea8fc5308f3069bfd", size = 45586, upload-time = "2025-12-06T19:04:29.116Z" }, + { url = "https://files.pythonhosted.org/packages/e2/df/8decd032ac9b995e4f5606cde783711a71094128d88d97a52e397daf2c89/librt-0.7.3-cp314-cp314t-win_amd64.whl", hash = "sha256:25711f364c64cab2c910a0247e90b51421e45dbc8910ceeb4eac97a9e132fc6f", size = 53002, upload-time = "2025-12-06T19:04:30.173Z" }, + { url = "https://files.pythonhosted.org/packages/de/0c/6605b6199de8178afe7efc77ca1d8e6db00453bc1d3349d27605c0f42104/librt-0.7.3-cp314-cp314t-win_arm64.whl", hash = "sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b", size = 45647, upload-time = "2025-12-06T19:04:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/e1/70/b3f19e3bb34f44e218c8271dc0b2b14eb6b183fbccbececf94c71e2b5e69/librt-0.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd8551aa21df6c60baa2624fd086ae7486bdde00c44097b32e1d1b1966e365e0", size = 54850, upload-time = "2025-12-06T19:04:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/6599ed7726aaa9b5bacea206d5861b94e76866240e2f394a59594bf3db46/librt-0.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6eb9295c730e26b849ed1f4022735f36863eb46b14b6e10604c1c39b8b5efaea", size = 56797, upload-time = "2025-12-06T19:04:34.193Z" }, + { url = "https://files.pythonhosted.org/packages/33/83/216db13224a6f688787f456909bbc50f9d951c0f4bea8ba38a2eb931d581/librt-0.7.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3edbf257c40d21a42615e9e332a6b10a8bacaaf58250aed8552a14a70efd0d65", size = 159681, upload-time = "2025-12-06T19:04:35.554Z" }, + { url = "https://files.pythonhosted.org/packages/83/23/0a490c8ba3bc90090647ac7b9b3c63c16af7378bcabe3ff4c7d7890d66e5/librt-0.7.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b29e97273bd6999e2bfe9fe3531b1f4f64effd28327bced048a33e49b99674a", size = 168505, upload-time = "2025-12-06T19:04:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/5e/16/b47c60805285caa06728d61d933fdd6db5b7321f375ce496cb7fdbeb1a44/librt-0.7.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e40520c37926166c24d0c2e0f3bc3a5f46646c34bdf7b4ea9747c297d6ee809", size = 182234, upload-time = "2025-12-06T19:04:37.889Z" }, + { url = "https://files.pythonhosted.org/packages/2d/2f/bef211d7f0d55fa2484d2c644b2cdae8c9c5eec050754b0516e6582ad452/librt-0.7.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6bdd9adfca615903578d2060ee8a6eb1c24eaf54919ff0ddc820118e5718931b", size = 178276, upload-time = "2025-12-06T19:04:39.408Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dd/5a3e7762b086b62fabb31fd4deaaf3ba888cfdd3b8f2e3247f076c18a6ff/librt-0.7.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f57aca20e637750a2c18d979f7096e2c2033cc40cf7ed201494318de1182f135", size = 172602, upload-time = "2025-12-06T19:04:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/533d5bfd5b377eb03ed54101814b530fc1f9bbe0e79971c641a3f15bfb33/librt-0.7.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cad9971881e4fec00d96af7eaf4b63aa7a595696fc221808b0d3ce7ca9743258", size = 192741, upload-time = "2025-12-06T19:04:41.738Z" }, + { url = "https://files.pythonhosted.org/packages/9f/69/0b87ce8e95f65ebc864f390f1139b8fe9fac6fb64b797307447b1719610c/librt-0.7.3-cp39-cp39-win32.whl", hash = "sha256:170cdb8436188347af17bf9cccf3249ba581c933ed56d926497119d4cf730cec", size = 47154, upload-time = "2025-12-06T19:04:42.96Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/070dee0add2d6e742be4d8b965d5a37c24562b43e8ef7deba8ed5b5d3c0f/librt-0.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:b278a9248a4e3260fee3db7613772ca9ab6763a129d6d6f29555e2f9b168216d", size = 54339, upload-time = "2025-12-06T19:04:44.415Z" }, ] [[package]] @@ -485,21 +690,55 @@ 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 = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -511,7 +750,7 @@ resolution-markers = [ ] dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version < '3.10'" }, - { name = "pytest", marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } @@ -528,7 +767,7 @@ resolution-markers = [ ] dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*'" }, - { name = "pytest", marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } @@ -538,23 +777,24 @@ wheels = [ [[package]] name = "pytest-mock" -version = "3.14.1" +version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] name = "python-dotenv" -version = "1.1.0" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] [[package]] @@ -586,66 +826,77 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707, upload-time = "2025-05-22T19:19:34.363Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049, upload-time = "2025-05-22T19:18:45.516Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601, upload-time = "2025-05-22T19:18:49.269Z" }, - { url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421, upload-time = "2025-05-22T19:18:51.754Z" }, - { url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980, upload-time = "2025-05-22T19:18:54.011Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241, upload-time = "2025-05-22T19:18:56.041Z" }, - { url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398, upload-time = "2025-05-22T19:18:58.248Z" }, - { url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955, upload-time = "2025-05-22T19:19:00.981Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803, upload-time = "2025-05-22T19:19:03.258Z" }, - { url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630, upload-time = "2025-05-22T19:19:05.871Z" }, - { url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310, upload-time = "2025-05-22T19:19:08.584Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144, upload-time = "2025-05-22T19:19:13.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987, upload-time = "2025-05-22T19:19:15.821Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922, upload-time = "2025-05-22T19:19:18.104Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537, upload-time = "2025-05-22T19:19:20.889Z" }, - { url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492, upload-time = "2025-05-22T19:19:23.642Z" }, - { url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562, upload-time = "2025-05-22T19:19:27.013Z" }, - { url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951, upload-time = "2025-05-22T19:19:30.043Z" }, +version = "0.14.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" }, + { url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" }, + { url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" }, + { url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" }, + { url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" }, + { url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" }, + { url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" }, + { url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, ] [[package]] name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] [[package]] @@ -658,11 +909,15 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "coverage" }, - { name = "faker" }, - { name = "isort" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "faker", version = "37.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "faker", version = "38.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "isort", version = "6.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "isort", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "mypy" }, - { name = "pytest" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-mock" }, @@ -695,11 +950,11 @@ dev = [ [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] @@ -719,3 +974,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/1d/0f3a93cca1ac5e828 wheels = [ { url = "https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b", size = 131138, upload-time = "2025-12-08T15:25:25.51Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 63c426f42feaec99fcbe15d75cd16120c8f0a5af Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:49:33 +0200 Subject: [PATCH 29/32] feat(stemming): add async support for stemming operations - add AsyncStemming class for async stemming dictionary operations - add AsyncStemmingDictionaries class for async dictionaries collection operations - add AsyncStemmingDictionary class for async individual dictionary operations - add async tests for stemming functionality - add async fixtures for testing async stemming operations --- src/typesense/async_stemming.py | 50 +++++ src/typesense/async_stemming_dictionaries.py | 185 +++++++++++++++++++ src/typesense/async_stemming_dictionary.py | 75 ++++++++ tests/fixtures/stemming_fixtures.py | 18 ++ tests/stemming_test.py | 39 ++++ 5 files changed, 367 insertions(+) create mode 100644 src/typesense/async_stemming.py create mode 100644 src/typesense/async_stemming_dictionaries.py create mode 100644 src/typesense/async_stemming_dictionary.py diff --git a/src/typesense/async_stemming.py b/src/typesense/async_stemming.py new file mode 100644 index 0000000..e1130f4 --- /dev/null +++ b/src/typesense/async_stemming.py @@ -0,0 +1,50 @@ +""" +Module for managing stemming dictionaries in Typesense (async). + +This module provides a class for managing stemming dictionaries in Typesense, +including creating, updating, and retrieving them asynchronously. + +Classes: + - AsyncStemming: Handles async operations related to stemming dictionaries. + +Attributes: + - AsyncStemmingDictionaries: The AsyncStemmingDictionaries object for managing stemming dictionaries. + +Methods: + - __init__: Initializes the AsyncStemming object. + +The AsyncStemming class interacts with the Typesense API to manage stemming dictionary operations. +It provides access to the AsyncStemmingDictionaries object for managing stemming dictionaries. + +For more information on stemming dictionaries, refer to the Stemming +[documentation](https://typesense.org/docs/28.0/api/stemming.html) + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.async_stemming_dictionaries import AsyncStemmingDictionaries + + +class AsyncStemming(object): + """ + Class for managing stemming dictionaries in Typesense (async). + + This class provides methods to interact with stemming dictionaries, including + creating, updating, and retrieving them. + + Attributes: + dictionaries (AsyncStemmingDictionaries): The AsyncStemmingDictionaries object for managing + stemming dictionaries. + """ + + def __init__(self, api_call: AsyncApiCall): + """ + Initialize the AsyncStemming object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + """ + self.api_call = api_call + self.dictionaries = AsyncStemmingDictionaries(api_call) diff --git a/src/typesense/async_stemming_dictionaries.py b/src/typesense/async_stemming_dictionaries.py new file mode 100644 index 0000000..9494e9f --- /dev/null +++ b/src/typesense/async_stemming_dictionaries.py @@ -0,0 +1,185 @@ +""" +Module for interacting with the stemming dictionaries endpoint of the Typesense API (async). + +This module provides a class for managing stemming dictionaries in Typesense, including creating +and updating them asynchronously. + +Classes: + - AsyncStemmingDictionaries: Handles async operations related to stemming dictionaries. + +Methods: + - __init__: Initializes the AsyncStemmingDictionaries object. + - __getitem__: Retrieves or creates an AsyncStemmingDictionary object for a given dictionary_id. + - upsert: Creates or updates a stemming dictionary. + - _upsert_list: Creates or updates a list of stemming dictionaries. + - _dump_to_jsonl: Dumps a list of StemmingDictionaryCreateSchema objects to a JSONL string. + - _parse_response: Parses the response from the upsert operation. + - _upsert_raw: Performs the raw upsert operation. + - _endpoint_path: Constructs the API endpoint path for this specific stemming dictionary. + +The AsyncStemmingDictionaries class interacts with the Typesense API to manage stemming dictionary +operations. It provides methods to create, update, and retrieve stemming dictionaries, as well as +access individual AsyncStemmingDictionary objects. + +For more information on stemming dictionaries, +refer to the Stemming [documentation](https://typesense.org/docs/28.0/api/stemming.html) +""" + +import json +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.async_stemming_dictionary import AsyncStemmingDictionary +from typesense.types.stemming import ( + StemmingDictionariesRetrieveSchema, + StemmingDictionaryCreateSchema, +) + + +class AsyncStemmingDictionaries: + """ + Class for managing stemming dictionaries in Typesense (async). + + This class provides methods to interact with stemming dictionaries, including + creating, updating, and retrieving them. + + Attributes: + api_call (AsyncApiCall): The API call object for making requests. + stemming_dictionaries (Dict[str, AsyncStemmingDictionary]): A dictionary of + AsyncStemmingDictionary objects. + """ + + resource_path: typing.Final[str] = "/stemming/dictionaries" + + def __init__(self, api_call: AsyncApiCall): + """ + Initialize the AsyncStemmingDictionaries object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + """ + self.api_call = api_call + self.stemming_dictionaries: typing.Dict[str, AsyncStemmingDictionary] = {} + + def __getitem__(self, dictionary_id: str) -> AsyncStemmingDictionary: + """ + Get or create an AsyncStemmingDictionary object for a given dictionary_id. + + Args: + dictionary_id (str): The ID of the stemming dictionary. + + Returns: + AsyncStemmingDictionary: The AsyncStemmingDictionary object for the given ID. + """ + if not self.stemming_dictionaries.get(dictionary_id): + self.stemming_dictionaries[dictionary_id] = AsyncStemmingDictionary( + self.api_call, + dictionary_id, + ) + return self.stemming_dictionaries[dictionary_id] + + async def retrieve(self) -> StemmingDictionariesRetrieveSchema: + """ + Retrieve the list of stemming dictionaries. + + Returns: + StemmingDictionariesRetrieveSchema: The list of stemming dictionaries. + """ + response: StemmingDictionariesRetrieveSchema = await self.api_call.get( + self._endpoint_path(), + entity_type=StemmingDictionariesRetrieveSchema, + ) + return response + + @typing.overload + async def upsert( + self, + dictionary_id: str, + word_root_combinations: typing.Union[str, bytes], + ) -> str: ... + + @typing.overload + async def upsert( + self, + dictionary_id: str, + word_root_combinations: typing.List[StemmingDictionaryCreateSchema], + ) -> typing.List[StemmingDictionaryCreateSchema]: ... + + async def upsert( + self, + dictionary_id: str, + word_root_combinations: typing.Union[ + typing.List[StemmingDictionaryCreateSchema], + str, + bytes, + ], + ) -> typing.Union[str, typing.List[StemmingDictionaryCreateSchema]]: + if isinstance(word_root_combinations, (str, bytes)): + return await self._upsert_raw(dictionary_id, word_root_combinations) + + return await self._upsert_list(dictionary_id, word_root_combinations) + + async def _upsert_list( + self, + dictionary_id: str, + word_root_combinations: typing.List[StemmingDictionaryCreateSchema], + ) -> typing.List[StemmingDictionaryCreateSchema]: + word_combos_in_jsonl = self._dump_to_jsonl(word_root_combinations) + response = await self._upsert_raw(dictionary_id, word_combos_in_jsonl) + return self._parse_response(response) + + def _dump_to_jsonl( + self, + word_root_combinations: typing.List[StemmingDictionaryCreateSchema], + ) -> str: + word_root_strs = [json.dumps(combo) for combo in word_root_combinations] + + return "\n".join(word_root_strs) + + def _parse_response( + self, + response: str, + ) -> typing.List[StemmingDictionaryCreateSchema]: + object_list: typing.List[StemmingDictionaryCreateSchema] = [] + + for line in response.split("\n"): + try: + decoded = json.loads(line) + except json.JSONDecodeError as err: + raise ValueError(f"Failed to parse JSON from response: {line}") from err + object_list.append(decoded) + return object_list + + async def _upsert_raw( + self, + dictionary_id: str, + word_root_combinations: typing.Union[bytes, str], + ) -> str: + response: str = await self.api_call.post( + self._endpoint_path("import"), + body=word_root_combinations, + as_json=False, + entity_type=str, + params={"id": dictionary_id}, + ) + return response + + def _endpoint_path(self, action: typing.Union[str, None] = None) -> str: + """ + Construct the API endpoint path for this specific stemming dictionary. + + Args: + action (str, optional): The action to perform on the stemming dictionary. + Defaults to None. + + Returns: + str: The constructed endpoint path. + """ + if action: + return f"{AsyncStemmingDictionaries.resource_path}/{action}" + return AsyncStemmingDictionaries.resource_path diff --git a/src/typesense/async_stemming_dictionary.py b/src/typesense/async_stemming_dictionary.py new file mode 100644 index 0000000..6d1f8a0 --- /dev/null +++ b/src/typesense/async_stemming_dictionary.py @@ -0,0 +1,75 @@ +""" +Module for managing individual stemming dictionaries in Typesense (async). + +This module provides a class for managing individual stemming dictionaries in Typesense, +including retrieving them asynchronously. + +Classes: + - AsyncStemmingDictionary: Handles async operations related to individual stemming dictionaries. + +Methods: + - __init__: Initializes the AsyncStemmingDictionary object. + - retrieve: Retrieves this specific stemming dictionary. + +The AsyncStemmingDictionary class interacts with the Typesense API to manage operations on a +specific stemming dictionary. It provides methods to retrieve the dictionary details. + +For more information on stemming dictionaries, refer to the Stemming +[documentation](https://typesense.org/docs/28.0/api/stemming.html) + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.types.stemming import StemmingDictionarySchema + + +class AsyncStemmingDictionary: + """ + Class for managing individual stemming dictionaries in Typesense (async). + + This class provides methods to interact with a specific stemming dictionary, + including retrieving it. + + Attributes: + api_call (AsyncApiCall): The API call object for making requests. + dict_id (str): The ID of the stemming dictionary. + """ + + def __init__(self, api_call: AsyncApiCall, dict_id: str): + """ + Initialize the AsyncStemmingDictionary object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + dict_id (str): The ID of the stemming dictionary. + """ + self.api_call = api_call + self.dict_id = dict_id + + async def retrieve(self) -> StemmingDictionarySchema: + """ + Retrieve this specific stemming dictionary. + + Returns: + StemmingDictionarySchema: The schema containing the stemming dictionary details. + """ + response: StemmingDictionarySchema = await self.api_call.get( + self._endpoint_path, + entity_type=StemmingDictionarySchema, + as_json=True, + ) + return response + + @property + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific stemming dictionary. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_stemming_dictionaries import AsyncStemmingDictionaries + + return "/".join([AsyncStemmingDictionaries.resource_path, self.dict_id]) diff --git a/tests/fixtures/stemming_fixtures.py b/tests/fixtures/stemming_fixtures.py index be571ed..ccaa3c5 100644 --- a/tests/fixtures/stemming_fixtures.py +++ b/tests/fixtures/stemming_fixtures.py @@ -3,6 +3,8 @@ import pytest from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_stemming import AsyncStemming from typesense.stemming import Stemming @@ -12,3 +14,19 @@ def actual_stemming_fixture( ) -> Stemming: """Return a Stemming object using a real API.""" return Stemming(actual_api_call) + + +@pytest.fixture(scope="function", name="actual_async_stemming") +def actual_async_stemming_fixture( + actual_async_api_call: AsyncApiCall, +) -> AsyncStemming: + """Return a AsyncStemming object using a real API.""" + return AsyncStemming(actual_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_stemming") +def fake_async_stemming_fixture( + fake_async_api_call: AsyncApiCall, +) -> AsyncStemming: + """Return a AsyncStemming object with test values.""" + return AsyncStemming(fake_async_api_call) diff --git a/tests/stemming_test.py b/tests/stemming_test.py index 9c0a812..7bc7aa1 100644 --- a/tests/stemming_test.py +++ b/tests/stemming_test.py @@ -1,5 +1,7 @@ """Tests for stemming.""" +from typesense.async_api_call import AsyncApiCall +from typesense.async_stemming import AsyncStemming from typesense.stemming import Stemming @@ -38,3 +40,40 @@ def test_actual_retrieve( {"word": "fishing", "root": "fish"}, ], } + + +async def test_actual_upsert_async( + actual_async_stemming: AsyncStemming, +) -> None: + """Test that it can upsert a stemming dictionary to Typesense Server.""" + response = await actual_async_stemming.dictionaries.upsert( + "set_1", + [{"word": "running", "root": "run"}, {"word": "fishing", "root": "fish"}], + ) + + assert response == [ + {"word": "running", "root": "run"}, + {"word": "fishing", "root": "fish"}, + ] + + +async def test_actual_retrieve_many_async( + actual_async_stemming: AsyncStemming, +) -> None: + """Test that it can retrieve all stemming dictionaries from Typesense Server.""" + response = await actual_async_stemming.dictionaries.retrieve() + assert response == {"dictionaries": ["set_1"]} + + +async def test_actual_retrieve_async( + actual_async_stemming: AsyncStemming, +) -> None: + """Test that it can retrieve a single stemming dictionary from Typesense Server.""" + response = await actual_async_stemming.dictionaries["set_1"].retrieve() + assert response == { + "id": "set_1", + "words": [ + {"word": "running", "root": "run"}, + {"word": "fishing", "root": "fish"}, + ], + } From 57ce53e0461e4522d9e0e08f0e06df1fba08bba1 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:50:01 +0200 Subject: [PATCH 30/32] feat(stopwords): add async support for stopwords operations - add AsyncStopwords class for async stopwords collection operations - add AsyncStopwordsSet class for async individual stopwords set operations - add async tests for stopwords and stopwords set functionality - add async fixtures for testing async stopwords operations --- src/typesense/async_stopwords.py | 117 +++++++++++++++++++++++++++ src/typesense/async_stopwords_set.py | 87 ++++++++++++++++++++ tests/fixtures/stopword_fixtures.py | 27 +++++++ tests/stopwords_set_test.py | 50 +++++++++++- tests/stopwords_test.py | 104 +++++++++++++++++++++++- 5 files changed, 381 insertions(+), 4 deletions(-) create mode 100644 src/typesense/async_stopwords.py create mode 100644 src/typesense/async_stopwords_set.py diff --git a/src/typesense/async_stopwords.py b/src/typesense/async_stopwords.py new file mode 100644 index 0000000..9586804 --- /dev/null +++ b/src/typesense/async_stopwords.py @@ -0,0 +1,117 @@ +""" +This module provides async functionality for managing stopwords in Typesense. + +Classes: + - AsyncStopwords: Handles async operations related to stopwords and stopword sets. + +Methods: + - __init__: Initializes the AsyncStopwords object. + - __getitem__: Retrieves or creates an AsyncStopwordsSet object for a given stopwords_set_id. + - upsert: Creates or updates a stopwords set. + - retrieve: Retrieves all stopwords sets. + +Attributes: + - RESOURCE_PATH: The API resource path for stopwords operations. + +The AsyncStopwords class interacts with the Typesense API to manage stopwords operations. +It provides methods to create, update, and retrieve stopwords sets, as well as access +individual AsyncStopwordsSet objects. + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +import sys + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_api_call import AsyncApiCall +from typesense.async_stopwords_set import AsyncStopwordsSet +from typesense.types.stopword import ( + StopwordCreateSchema, + StopwordSchema, + StopwordsRetrieveSchema, +) + + +class AsyncStopwords: + """ + Class for managing stopwords in Typesense (async). + + This class provides methods to interact with stopwords and stopwords sets, including + creating, updating, retrieving, and accessing individual stopwords sets. + + Attributes: + RESOURCE_PATH (str): The API resource path for stopwords operations. + api_call (AsyncApiCall): The API call object for making requests. + stopwords_sets (Dict[str, AsyncStopwordsSet]): A dictionary of AsyncStopwordsSet objects. + """ + + resource_path: typing.Final[str] = "/stopwords" + + def __init__(self, api_call: AsyncApiCall): + """ + Initialize the AsyncStopwords object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + """ + self.api_call = api_call + self.stopwords_sets: typing.Dict[str, AsyncStopwordsSet] = {} + + def __getitem__(self, stopwords_set_id: str) -> AsyncStopwordsSet: + """ + Get or create an AsyncStopwordsSet object for a given stopwords_set_id. + + Args: + stopwords_set_id (str): The ID of the stopwords set. + + Returns: + AsyncStopwordsSet: The AsyncStopwordsSet object for the given ID. + """ + if not self.stopwords_sets.get(stopwords_set_id): + self.stopwords_sets[stopwords_set_id] = AsyncStopwordsSet( + self.api_call, + stopwords_set_id, + ) + return self.stopwords_sets[stopwords_set_id] + + async def upsert( + self, + stopwords_set_id: str, + stopwords_set: StopwordCreateSchema, + ) -> StopwordSchema: + """ + Create or update a stopwords set. + + Args: + stopwords_set_id (str): The ID of the stopwords set to upsert. + stopwords_set (StopwordCreateSchema): + The schema for creating or updating the stopwords set. + + Returns: + StopwordSchema: The created or updated stopwords set. + """ + response: StopwordSchema = await self.api_call.put( + "/".join([AsyncStopwords.resource_path, stopwords_set_id]), + body=stopwords_set, + entity_type=StopwordSchema, + ) + return response + + async def retrieve(self) -> StopwordsRetrieveSchema: + """ + Retrieve all stopwords sets. + + Returns: + StopwordsRetrieveSchema: The schema containing all stopwords sets. + """ + response: StopwordsRetrieveSchema = await self.api_call.get( + AsyncStopwords.resource_path, + as_json=True, + entity_type=StopwordsRetrieveSchema, + ) + return response diff --git a/src/typesense/async_stopwords_set.py b/src/typesense/async_stopwords_set.py new file mode 100644 index 0000000..dc9359d --- /dev/null +++ b/src/typesense/async_stopwords_set.py @@ -0,0 +1,87 @@ +""" +This module provides async functionality for managing individual stopwords sets in Typesense. + +Classes: + - AsyncStopwordsSet: Handles async operations related to a specific stopwords set. + +Methods: + - __init__: Initializes the AsyncStopwordsSet object. + - retrieve: Retrieves the details of this specific stopwords set. + - delete: Deletes this specific stopwords set. + - _endpoint_path: Constructs the API endpoint path for this specific stopwords set. + +The AsyncStopwordsSet class interacts with the Typesense API to manage operations on a +specific stopwords set. It provides methods to retrieve and delete individual stopwords sets. + +For more information regarding Stopwords, refer to the Stopwords [documentation] +(https://typesense.org/docs/27.0/api/stopwords.html). + +This module uses type hinting and is compatible with Python 3.11+ as well as earlier +versions through the use of the typing_extensions library. +""" + +from typesense.async_api_call import AsyncApiCall +from typesense.types.stopword import StopwordDeleteSchema, StopwordsSingleRetrieveSchema + + +class AsyncStopwordsSet: + """ + Class for managing individual stopwords sets in Typesense (async). + + This class provides methods to interact with a specific stopwords set, + including retrieving and deleting it. + + Attributes: + stopwords_set_id (str): The ID of the stopwords set. + api_call (AsyncApiCall): The API call object for making requests. + """ + + def __init__(self, api_call: AsyncApiCall, stopwords_set_id: str) -> None: + """ + Initialize the AsyncStopwordsSet object. + + Args: + api_call (AsyncApiCall): The API call object for making requests. + stopwords_set_id (str): The ID of the stopwords set. + """ + self.stopwords_set_id = stopwords_set_id + self.api_call = api_call + + async def retrieve(self) -> StopwordsSingleRetrieveSchema: + """ + Retrieve this specific stopwords set. + + Returns: + StopwordsSingleRetrieveSchema: The schema containing the stopwords set details. + """ + response: StopwordsSingleRetrieveSchema = await self.api_call.get( + self._endpoint_path, + entity_type=StopwordsSingleRetrieveSchema, + as_json=True, + ) + return response + + async def delete(self) -> StopwordDeleteSchema: + """ + Delete this specific stopwords set. + + Returns: + StopwordDeleteSchema: The schema containing the deletion response. + """ + response: StopwordDeleteSchema = await self.api_call.delete( + self._endpoint_path, + entity_type=StopwordDeleteSchema, + ) + return response + + @property + def _endpoint_path(self) -> str: + """ + Construct the API endpoint path for this specific stopwords set. + + Returns: + str: The constructed endpoint path. + """ + from typesense.async_stopwords import AsyncStopwords + + return "/".join([AsyncStopwords.resource_path, self.stopwords_set_id]) diff --git a/tests/fixtures/stopword_fixtures.py b/tests/fixtures/stopword_fixtures.py index eb4bb2d..f31fcb2 100644 --- a/tests/fixtures/stopword_fixtures.py +++ b/tests/fixtures/stopword_fixtures.py @@ -4,6 +4,9 @@ import requests from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_stopwords import AsyncStopwords +from typesense.async_stopwords_set import AsyncStopwordsSet from typesense.stopwords import Stopwords from typesense.stopwords_set import StopwordsSet @@ -67,3 +70,27 @@ def fake_stopwords_fixture(fake_api_call: ApiCall) -> Stopwords: def fake_stopwords_set_fixture(fake_api_call: ApiCall) -> StopwordsSet: """Return a Collection object with test values.""" return StopwordsSet(fake_api_call, "company_stopwords") + + +@pytest.fixture(scope="function", name="actual_async_stopwords") +def actual_async_stopwords_fixture(actual_async_api_call: AsyncApiCall) -> AsyncStopwords: + """Return a AsyncStopwords object using a real API.""" + return AsyncStopwords(actual_async_api_call) + + +@pytest.fixture(scope="function", name="actual_async_stopwords_set") +def actual_async_stopwords_set_fixture(actual_async_api_call: AsyncApiCall) -> AsyncStopwordsSet: + """Return a AsyncStopwordsSet object using a real API.""" + return AsyncStopwordsSet(actual_async_api_call, "company_stopwords") + + +@pytest.fixture(scope="function", name="fake_async_stopwords") +def fake_async_stopwords_fixture(fake_async_api_call: AsyncApiCall) -> AsyncStopwords: + """Return a AsyncStopwords object with test values.""" + return AsyncStopwords(fake_async_api_call) + + +@pytest.fixture(scope="function", name="fake_async_stopwords_set") +def fake_async_stopwords_set_fixture(fake_async_api_call: AsyncApiCall) -> AsyncStopwordsSet: + """Return a AsyncStopwordsSet object with test values.""" + return AsyncStopwordsSet(fake_async_api_call, "company_stopwords") diff --git a/tests/stopwords_set_test.py b/tests/stopwords_set_test.py index 1bbc581..04ad43a 100644 --- a/tests/stopwords_set_test.py +++ b/tests/stopwords_set_test.py @@ -1,9 +1,9 @@ """Tests for the StopwordsSet class.""" - - from tests.utils.object_assertions import assert_match_object, assert_object_lists_match from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_stopwords import AsyncStopwords from typesense.stopwords import Stopwords from typesense.stopwords_set import StopwordsSet from typesense.types.stopword import StopwordDeleteSchema, StopwordSchema @@ -51,3 +51,49 @@ def test_actual_delete( response = actual_stopwords["company_stopwords"].delete() assert response == {"id": "company_stopwords"} + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncStopwordsSet object is initialized correctly.""" + from typesense.async_stopwords_set import AsyncStopwordsSet + + stopword_set = AsyncStopwordsSet(fake_async_api_call, "company_stopwords") + + assert stopword_set.stopwords_set_id == "company_stopwords" + assert_match_object(stopword_set.api_call, fake_async_api_call) + assert_object_lists_match( + stopword_set.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + stopword_set.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + assert stopword_set._endpoint_path == "/stopwords/company_stopwords" # noqa: WPS437 + + +async def test_actual_retrieve_async( + actual_async_stopwords: AsyncStopwords, + delete_all_stopwords: None, + delete_all: None, + create_stopword: None, +) -> None: + """Test that the AsyncStopwordsSet object can retrieve an stopword_set from Typesense Server.""" + response = await actual_async_stopwords["company_stopwords"].retrieve() + + assert response == { + "stopwords": { + "id": "company_stopwords", + "stopwords": ["and", "is", "the"], + }, + } + + +async def test_actual_delete_async( + actual_async_stopwords: AsyncStopwords, + create_stopword: None, +) -> None: + """Test that the AsyncStopwordsSet object can delete an stopword_set from Typesense Server.""" + response = await actual_async_stopwords["company_stopwords"].delete() + + assert response == {"id": "company_stopwords"} diff --git a/tests/stopwords_test.py b/tests/stopwords_test.py index c69f4c0..4c9494e 100644 --- a/tests/stopwords_test.py +++ b/tests/stopwords_test.py @@ -1,13 +1,13 @@ """Tests for the Stopwords class.""" - - from tests.utils.object_assertions import ( assert_match_object, assert_object_lists_match, assert_to_contain_object, ) from typesense.api_call import ApiCall +from typesense.async_api_call import AsyncApiCall +from typesense.async_stopwords import AsyncStopwords from typesense.stopwords import Stopwords from typesense.types.stopword import StopwordSchema, StopwordsRetrieveSchema @@ -110,3 +110,103 @@ def test_actual_retrieve( "stopwords": ["and", "is", "the"], }, ) + + +def test_init_async(fake_async_api_call: AsyncApiCall) -> None: + """Test that the AsyncStopwords object is initialized correctly.""" + stopwords = AsyncStopwords(fake_async_api_call) + + assert_match_object(stopwords.api_call, fake_async_api_call) + assert_object_lists_match( + stopwords.api_call.node_manager.nodes, + fake_async_api_call.node_manager.nodes, + ) + assert_match_object( + stopwords.api_call.config.nearest_node, + fake_async_api_call.config.nearest_node, + ) + + assert not stopwords.stopwords_sets + + +def test_get_missing_stopword_async(fake_async_stopwords: AsyncStopwords) -> None: + """Test that the AsyncStopwords object can get a missing stopword.""" + stopword = fake_async_stopwords["company_stopwords"] + + assert stopword.stopwords_set_id == "company_stopwords" + assert_match_object(stopword.api_call, fake_async_stopwords.api_call) + assert_object_lists_match( + stopword.api_call.node_manager.nodes, fake_async_stopwords.api_call.node_manager.nodes + ) + assert_match_object( + stopword.api_call.config.nearest_node, + fake_async_stopwords.api_call.config.nearest_node, + ) + assert stopword._endpoint_path == "/stopwords/company_stopwords" # noqa: WPS437 + + +def test_get_existing_stopword_async(fake_async_stopwords: AsyncStopwords) -> None: + """Test that the AsyncStopwords object can get an existing stopword.""" + stopword = fake_async_stopwords["company_stopwords"] + fetched_stopword = fake_async_stopwords["company_stopwords"] + + assert len(fake_async_stopwords.stopwords_sets) == 1 + + assert stopword is fetched_stopword + + +async def test_actual_create_async(actual_async_stopwords: AsyncStopwords, delete_all_stopwords: None) -> None: + """Test that the AsyncStopwords object can create an stopword on Typesense Server.""" + response = await actual_async_stopwords.upsert( + "company_stopwords", + {"stopwords": ["and", "is", "the"]}, + ) + + assert response == { + "id": "company_stopwords", + "stopwords": ["and", "is", "the"], + } + + +async def test_actual_update_async( + actual_async_stopwords: AsyncStopwords, + delete_all_stopwords: None, +) -> None: + """Test that the AsyncStopwords object can update an stopword on Typesense Server.""" + create_response = await actual_async_stopwords.upsert( + "company_stopwords", + {"stopwords": ["and", "is", "the"]}, + ) + + assert create_response == { + "id": "company_stopwords", + "stopwords": ["and", "is", "the"], + } + + update_response = await actual_async_stopwords.upsert( + "company_stopwords", + {"stopwords": ["and", "is", "other"]}, + ) + + assert update_response == { + "id": "company_stopwords", + "stopwords": ["and", "is", "other"], + } + + +async def test_actual_retrieve_async( + delete_all_stopwords: None, + create_stopword: None, + actual_async_stopwords: AsyncStopwords, +) -> None: + """Test that the AsyncStopwords object can retrieve an stopword from Typesense Server.""" + response = await actual_async_stopwords.retrieve() + + assert len(response["stopwords"]) == 1 + assert_to_contain_object( + response["stopwords"][0], + { + "id": "company_stopwords", + "stopwords": ["and", "is", "the"], + }, + ) From 95a4da58fb531a93090ca4964a96fc697a2ca98e Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Mon, 8 Dec 2025 20:52:48 +0200 Subject: [PATCH 31/32] feat(client): expose async client class --- src/typesense/async_client.py | 168 ++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/typesense/async_client.py diff --git a/src/typesense/async_client.py b/src/typesense/async_client.py new file mode 100644 index 0000000..df018f1 --- /dev/null +++ b/src/typesense/async_client.py @@ -0,0 +1,168 @@ +""" +This module provides the main async client interface for interacting with the Typesense API. + +It contains the AsyncClient class, which serves as the entry point for all Typesense operations, +integrating various components like collections, multi-search, keys, aliases, analytics, etc. + +Classes: + Client: The main client class for interacting with Typesense. + +Dependencies: + - typesense.aliases: Provides the AsyncAliases class. + - typesense.analytics: Provides the AsyncAnalytics class. + - typesense.api_call: Provides the AsyncApiCall class for making API requests. + - typesense.collection: Provides the AsyncCollection class. + - typesense.collections: Provides the AsyncCollections class. + - typesense.configuration: Provides AsyncConfiguration and ConfigDict types. + - typesense.conversations_models: Provides the AsyncConversationsModels class. + - typesense.debug: Provides the AsyncDebug class. + - typesense.keys: Provides the AsyncKeys class. + - typesense.metrics: Provides the AsyncMetrics class. + - typesense.multi_search: Provides the AsyncMultiSearch class. + - typesense.operations: Provides the AsyncOperations class. + - typesense.stopwords: Provides the AsyncStopwords class. + - typesense.types.document: Provides the AsyncDocumentSchema type. + +Note: This module uses conditional imports to support both Python 3.11+ and earlier versions. +""" + +import sys + +from typing_extensions import deprecated + +from typesense.types.document import DocumentSchema + +if sys.version_info >= (3, 11): + import typing +else: + import typing_extensions as typing + +from typesense.async_aliases import AsyncAliases +from typesense.async_analytics import AsyncAnalytics +from typesense.async_analytics_v1 import AsyncAnalyticsV1 +from typesense.async_api_call import AsyncApiCall +from typesense.async_collection import AsyncCollection +from typesense.async_collections import AsyncCollections +from typesense.async_conversations_models import AsyncConversationsModels +from typesense.async_curation_sets import AsyncCurationSets +from typesense.async_debug import AsyncDebug +from typesense.async_keys import AsyncKeys +from typesense.async_metrics import AsyncMetrics +from typesense.async_multi_search import AsyncMultiSearch +from typesense.async_nl_search_models import AsyncNLSearchModels +from typesense.async_operations import AsyncOperations +from typesense.async_stemming import AsyncStemming +from typesense.async_stopwords import AsyncStopwords +from typesense.async_synonym_sets import AsyncSynonymSets +from typesense.configuration import ConfigDict, Configuration + +TDoc = typing.TypeVar("TDoc", bound=DocumentSchema) + + +class AsyncClient: + """ + The main client class for interacting with Typesense. + + This class serves as the entry point for all Typesense operations. It initializes + and provides access to various components of the Typesense SDK, such as collections, + multi-search, keys, aliases, analytics, stemming, operations, debug, stopwords, + and conversation models. + + Attributes: + config (Configuration): The configuration object for the Typesense client. + api_call (ApiCall): The ApiCall instance for making API requests. + collections (Collections[DocumentSchema]): Instance for managing collections. + multi_search (MultiSearch): Instance for performing multi-search operations. + keys (Keys): Instance for managing API keys. + aliases (Aliases): Instance for managing collection aliases. + analyticsV1 (AnalyticsV1): Instance for analytics operations (V1). + analytics (Analytics): Instance for analytics operations (v30). + curation_sets (CurationSets): Instance for Curation Sets (v30+) + stemming (Stemming): Instance for stemming dictionary operations. + operations (Operations): Instance for various Typesense operations. + debug (Debug): Instance for debug operations. + stopwords (Stopwords): Instance for managing stopwords. + metrics (Metrics): Instance for retrieving system and Typesense metrics. + conversations_models (ConversationsModels): Instance for managing conversation models. + """ + + def __init__(self, config_dict: ConfigDict) -> None: + """ + Initialize the Client instance. + + Args: + config_dict (ConfigDict): + A dictionary containing the configuration for the Typesense client. + + Example: + >>> config = { + ... "api_key": "your_api_key", + ... "nodes": [ + ... {"host": "localhost", "port": "8108", "protocol": "http"} + ... ], + ... "connection_timeout_seconds": 2, + ... } + >>> client = Client(config) + """ + self.config = Configuration(config_dict) + self.api_call = AsyncApiCall(self.config) + self.collections: AsyncCollections[DocumentSchema] = AsyncCollections( + self.api_call + ) + self.multi_search = AsyncMultiSearch(self.api_call) + self.keys = AsyncKeys(self.api_call) + self.aliases = AsyncAliases(self.api_call) + self._analyticsV1 = AsyncAnalyticsV1(self.api_call) + self.analytics = AsyncAnalytics(self.api_call) + self.stemming = AsyncStemming(self.api_call) + self.curation_sets = AsyncCurationSets(self.api_call) + self.operations = AsyncOperations(self.api_call) + self.debug = AsyncDebug(self.api_call) + self.stopwords = AsyncStopwords(self.api_call) + self.synonym_sets = AsyncSynonymSets(self.api_call) + self.metrics = AsyncMetrics(self.api_call) + self.conversations_models = AsyncConversationsModels(self.api_call) + self.nl_search_models = AsyncNLSearchModels(self.api_call) + + @property + @deprecated( + "AnalyticsV1 is deprecated on v30+. Use client.analytics instead.", + category=None, + ) + def analyticsV1(self) -> AsyncAnalyticsV1: + return self._analyticsV1 + + def typed_collection( + self, + *, + model: typing.Type[TDoc], + name: typing.Union[str, None] = None, + ) -> AsyncCollection[TDoc]: + """ + Get a AsyncCollection instance for a specific document model. + + This method allows retrieving a AsyncCollection instance typed to a specific document model. + If no name is provided, it uses the lowercase name of the model class as + the collection name. + + Args: + model (Type[TDoc]): The document model class. + name (Union[str, None], optional): + The name of the collection. If None, uses the lowercase model class name. + + Returns: + AsyncCollection[TDoc]: An AsyncCollection instance typed to the specified document model. + + Example: + >>> class Company(DocumentSchema): + ... name: str + ... num_employees: int + >>> client = Client(config) + >>> companies_collection = client.typed_collection(model=Company) + # This is equivalent to: + # companies_collection = client.typed_collection(model=Company, name="company") + """ + if name is None: + name = model.__name__.lower() + collection: AsyncCollection[TDoc] = self.collections[name] + return collection From 953351625e5fcfceb4c0e0635ea1ccc1127ace14 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 9 Dec 2025 11:16:08 +0200 Subject: [PATCH 32/32] chore: bump version to 2.0.0 --- src/typesense/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typesense/__init__.py b/src/typesense/__init__.py index 147e6a8..0c1237e 100644 --- a/src/typesense/__init__.py +++ b/src/typesense/__init__.py @@ -1,4 +1,4 @@ from .client import Client # NOQA -__version__ = "1.3.0" +__version__ = "2.0.0"