From 8c11b910bf76fb17bc0048e9eedf693ef69baaf1 Mon Sep 17 00:00:00 2001 From: Denis Rozhnovskiy Date: Thu, 5 Sep 2024 11:42:46 +0500 Subject: [PATCH 01/24] refactor: pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bbfe5c8..d5445e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyoutlineapi" -version = "0.1.2" +version = "0.1.3" description = "A Python package to interact with the Outline VPN Server API" authors = ["Denis Rozhnovskiy "] readme = "README.md" From 00ab3c21f778114163ff825dd7350f5abd150f09 Mon Sep 17 00:00:00 2001 From: Denis Rozhnovskiy Date: Fri, 6 Sep 2024 16:05:41 +0500 Subject: [PATCH 02/24] docs: update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9d0a92d..33d5ce9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ integrating with bots and other automated systems that require accurate and secu [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![tests](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml/badge.svg)](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml) +![PyPI - Downloads](https://img.shields.io/pypi/dm/pyoutlineapi) ## Features From d0e776ff5c8e91fe1a85254232f56e04ebf265a6 Mon Sep 17 00:00:00 2001 From: Denis Rozhnovskiy Date: Sun, 8 Sep 2024 22:16:39 +0500 Subject: [PATCH 03/24] deps: update pydantic-core (2.20.1 -> 2.23.2),tzdata (2024.1), pydantic (2.8.2 -> 2.9.0) --- poetry.lock | 200 ++++++++++++++++++++++++++++------------------------ 1 file changed, 106 insertions(+), 94 deletions(-) diff --git a/poetry.lock b/poetry.lock index 247d31d..8dd72a4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -283,122 +283,123 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "2.8.2" +version = "2.9.0" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-2.9.0-py3-none-any.whl", hash = "sha256:f66a7073abd93214a20c5f7b32d56843137a7a2e70d02111f3be287035c45370"}, + {file = "pydantic-2.9.0.tar.gz", hash = "sha256:c7a8a9fdf7d100afa49647eae340e2d23efa382466a8d177efcd1381e9be5598"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" +pydantic-core = "2.23.2" typing-extensions = [ {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, ] +tzdata = {version = "*", markers = "python_version >= \"3.9\""} [package.extras] email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.20.1" +version = "2.23.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, + {file = "pydantic_core-2.23.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7d0324a35ab436c9d768753cbc3c47a865a2cbc0757066cb864747baa61f6ece"}, + {file = "pydantic_core-2.23.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:276ae78153a94b664e700ac362587c73b84399bd1145e135287513442e7dfbc7"}, + {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c7aa318da542cdcc60d4a648377ffe1a2ef0eb1e996026c7f74507b720a78"}, + {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1cf842265a3a820ebc6388b963ead065f5ce8f2068ac4e1c713ef77a67b71f7c"}, + {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae90b9e50fe1bd115b24785e962b51130340408156d34d67b5f8f3fa6540938e"}, + {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ae65fdfb8a841556b52935dfd4c3f79132dc5253b12c0061b96415208f4d622"}, + {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c8aa40f6ca803f95b1c1c5aeaee6237b9e879e4dfb46ad713229a63651a95fb"}, + {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c53100c8ee5a1e102766abde2158077d8c374bee0639201f11d3032e3555dfbc"}, + {file = "pydantic_core-2.23.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6b9dd6aa03c812017411734e496c44fef29b43dba1e3dd1fa7361bbacfc1354"}, + {file = "pydantic_core-2.23.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b18cf68255a476b927910c6873d9ed00da692bb293c5b10b282bd48a0afe3ae2"}, + {file = "pydantic_core-2.23.2-cp310-none-win32.whl", hash = "sha256:e460475719721d59cd54a350c1f71c797c763212c836bf48585478c5514d2854"}, + {file = "pydantic_core-2.23.2-cp310-none-win_amd64.whl", hash = "sha256:5f3cf3721eaf8741cffaf092487f1ca80831202ce91672776b02b875580e174a"}, + {file = "pydantic_core-2.23.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7ce8e26b86a91e305858e018afc7a6e932f17428b1eaa60154bd1f7ee888b5f8"}, + {file = "pydantic_core-2.23.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e9b24cca4037a561422bf5dc52b38d390fb61f7bfff64053ce1b72f6938e6b2"}, + {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753294d42fb072aa1775bfe1a2ba1012427376718fa4c72de52005a3d2a22178"}, + {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:257d6a410a0d8aeb50b4283dea39bb79b14303e0fab0f2b9d617701331ed1515"}, + {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8319e0bd6a7b45ad76166cc3d5d6a36c97d0c82a196f478c3ee5346566eebfd"}, + {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a05c0240f6c711eb381ac392de987ee974fa9336071fb697768dfdb151345ce"}, + {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d5b0ff3218858859910295df6953d7bafac3a48d5cd18f4e3ed9999efd2245f"}, + {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96ef39add33ff58cd4c112cbac076726b96b98bb8f1e7f7595288dcfb2f10b57"}, + {file = "pydantic_core-2.23.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0102e49ac7d2df3379ef8d658d3bc59d3d769b0bdb17da189b75efa861fc07b4"}, + {file = "pydantic_core-2.23.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6612c2a844043e4d10a8324c54cdff0042c558eef30bd705770793d70b224aa"}, + {file = "pydantic_core-2.23.2-cp311-none-win32.whl", hash = "sha256:caffda619099cfd4f63d48462f6aadbecee3ad9603b4b88b60cb821c1b258576"}, + {file = "pydantic_core-2.23.2-cp311-none-win_amd64.whl", hash = "sha256:6f80fba4af0cb1d2344869d56430e304a51396b70d46b91a55ed4959993c0589"}, + {file = "pydantic_core-2.23.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c83c64d05ffbbe12d4e8498ab72bdb05bcc1026340a4a597dc647a13c1605ec"}, + {file = "pydantic_core-2.23.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6294907eaaccf71c076abdd1c7954e272efa39bb043161b4b8aa1cd76a16ce43"}, + {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a801c5e1e13272e0909c520708122496647d1279d252c9e6e07dac216accc41"}, + {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc0c316fba3ce72ac3ab7902a888b9dc4979162d320823679da270c2d9ad0cad"}, + {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b06c5d4e8701ac2ba99a2ef835e4e1b187d41095a9c619c5b185c9068ed2a49"}, + {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82764c0bd697159fe9947ad59b6db6d7329e88505c8f98990eb07e84cc0a5d81"}, + {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b1a195efd347ede8bcf723e932300292eb13a9d2a3c1f84eb8f37cbbc905b7f"}, + {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7efb12e5071ad8d5b547487bdad489fbd4a5a35a0fc36a1941517a6ad7f23e0"}, + {file = "pydantic_core-2.23.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5dd0ec5f514ed40e49bf961d49cf1bc2c72e9b50f29a163b2cc9030c6742aa73"}, + {file = "pydantic_core-2.23.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:820f6ee5c06bc868335e3b6e42d7ef41f50dfb3ea32fbd523ab679d10d8741c0"}, + {file = "pydantic_core-2.23.2-cp312-none-win32.whl", hash = "sha256:3713dc093d5048bfaedbba7a8dbc53e74c44a140d45ede020dc347dda18daf3f"}, + {file = "pydantic_core-2.23.2-cp312-none-win_amd64.whl", hash = "sha256:e1895e949f8849bc2757c0dbac28422a04be031204df46a56ab34bcf98507342"}, + {file = "pydantic_core-2.23.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:da43cbe593e3c87d07108d0ebd73771dc414488f1f91ed2e204b0370b94b37ac"}, + {file = "pydantic_core-2.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:64d094ea1aa97c6ded4748d40886076a931a8bf6f61b6e43e4a1041769c39dd2"}, + {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:084414ffe9a85a52940b49631321d636dadf3576c30259607b75516d131fecd0"}, + {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043ef8469f72609c4c3a5e06a07a1f713d53df4d53112c6d49207c0bd3c3bd9b"}, + {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3649bd3ae6a8ebea7dc381afb7f3c6db237fc7cebd05c8ac36ca8a4187b03b30"}, + {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6db09153d8438425e98cdc9a289c5fade04a5d2128faff8f227c459da21b9703"}, + {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5668b3173bb0b2e65020b60d83f5910a7224027232c9f5dc05a71a1deac9f960"}, + {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c7b81beaf7c7ebde978377dc53679c6cba0e946426fc7ade54251dfe24a7604"}, + {file = "pydantic_core-2.23.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ae579143826c6f05a361d9546446c432a165ecf1c0b720bbfd81152645cb897d"}, + {file = "pydantic_core-2.23.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:19f1352fe4b248cae22a89268720fc74e83f008057a652894f08fa931e77dced"}, + {file = "pydantic_core-2.23.2-cp313-none-win32.whl", hash = "sha256:e1a79ad49f346aa1a2921f31e8dbbab4d64484823e813a002679eaa46cba39e1"}, + {file = "pydantic_core-2.23.2-cp313-none-win_amd64.whl", hash = "sha256:582871902e1902b3c8e9b2c347f32a792a07094110c1bca6c2ea89b90150caac"}, + {file = "pydantic_core-2.23.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:743e5811b0c377eb830150d675b0847a74a44d4ad5ab8845923d5b3a756d8100"}, + {file = "pydantic_core-2.23.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6650a7bbe17a2717167e3e23c186849bae5cef35d38949549f1c116031b2b3aa"}, + {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56e6a12ec8d7679f41b3750ffa426d22b44ef97be226a9bab00a03365f217b2b"}, + {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810ca06cca91de9107718dc83d9ac4d2e86efd6c02cba49a190abcaf33fb0472"}, + {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:785e7f517ebb9890813d31cb5d328fa5eda825bb205065cde760b3150e4de1f7"}, + {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ef71ec876fcc4d3bbf2ae81961959e8d62f8d74a83d116668409c224012e3af"}, + {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d50ac34835c6a4a0d456b5db559b82047403c4317b3bc73b3455fefdbdc54b0a"}, + {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16b25a4a120a2bb7dab51b81e3d9f3cde4f9a4456566c403ed29ac81bf49744f"}, + {file = "pydantic_core-2.23.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:41ae8537ad371ec018e3c5da0eb3f3e40ee1011eb9be1da7f965357c4623c501"}, + {file = "pydantic_core-2.23.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07049ec9306ec64e955b2e7c40c8d77dd78ea89adb97a2013d0b6e055c5ee4c5"}, + {file = "pydantic_core-2.23.2-cp38-none-win32.whl", hash = "sha256:086c5db95157dc84c63ff9d96ebb8856f47ce113c86b61065a066f8efbe80acf"}, + {file = "pydantic_core-2.23.2-cp38-none-win_amd64.whl", hash = "sha256:67b6655311b00581914aba481729971b88bb8bc7996206590700a3ac85e457b8"}, + {file = "pydantic_core-2.23.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:358331e21a897151e54d58e08d0219acf98ebb14c567267a87e971f3d2a3be59"}, + {file = "pydantic_core-2.23.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c4d9f15ffe68bcd3898b0ad7233af01b15c57d91cd1667f8d868e0eacbfe3f87"}, + {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0123655fedacf035ab10c23450163c2f65a4174f2bb034b188240a6cf06bb123"}, + {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e6e3ccebdbd6e53474b0bb7ab8b88e83c0cfe91484b25e058e581348ee5a01a5"}, + {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc535cb898ef88333cf317777ecdfe0faac1c2a3187ef7eb061b6f7ecf7e6bae"}, + {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aab9e522efff3993a9e98ab14263d4e20211e62da088298089a03056980a3e69"}, + {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05b366fb8fe3d8683b11ac35fa08947d7b92be78ec64e3277d03bd7f9b7cda79"}, + {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7568f682c06f10f30ef643a1e8eec4afeecdafde5c4af1b574c6df079e96f96c"}, + {file = "pydantic_core-2.23.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cdd02a08205dc90238669f082747612cb3c82bd2c717adc60f9b9ecadb540f80"}, + {file = "pydantic_core-2.23.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a2ab4f410f4b886de53b6bddf5dd6f337915a29dd9f22f20f3099659536b2f6"}, + {file = "pydantic_core-2.23.2-cp39-none-win32.whl", hash = "sha256:0448b81c3dfcde439551bb04a9f41d7627f676b12701865c8a2574bcea034437"}, + {file = "pydantic_core-2.23.2-cp39-none-win_amd64.whl", hash = "sha256:4cebb9794f67266d65e7e4cbe5dcf063e29fc7b81c79dc9475bd476d9534150e"}, + {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e758d271ed0286d146cf7c04c539a5169a888dd0b57026be621547e756af55bc"}, + {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f477d26183e94eaafc60b983ab25af2a809a1b48ce4debb57b343f671b7a90b6"}, + {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da3131ef2b940b99106f29dfbc30d9505643f766704e14c5d5e504e6a480c35e"}, + {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329a721253c7e4cbd7aad4a377745fbcc0607f9d72a3cc2102dd40519be75ed2"}, + {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7706e15cdbf42f8fab1e6425247dfa98f4a6f8c63746c995d6a2017f78e619ae"}, + {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e64ffaf8f6e17ca15eb48344d86a7a741454526f3a3fa56bc493ad9d7ec63936"}, + {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dd59638025160056687d598b054b64a79183f8065eae0d3f5ca523cde9943940"}, + {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:12625e69b1199e94b0ae1c9a95d000484ce9f0182f9965a26572f054b1537e44"}, + {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d813fd871b3d5c3005157622ee102e8908ad6011ec915a18bd8fde673c4360e"}, + {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1eb37f7d6a8001c0f86dc8ff2ee8d08291a536d76e49e78cda8587bb54d8b329"}, + {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ce7eaf9a98680b4312b7cebcdd9352531c43db00fca586115845df388f3c465"}, + {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f087879f1ffde024dd2788a30d55acd67959dcf6c431e9d3682d1c491a0eb474"}, + {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ce883906810b4c3bd90e0ada1f9e808d9ecf1c5f0b60c6b8831d6100bcc7dd6"}, + {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a8031074a397a5925d06b590121f8339d34a5a74cfe6970f8a1124eb8b83f4ac"}, + {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23af245b8f2f4ee9e2c99cb3f93d0e22fb5c16df3f2f643f5a8da5caff12a653"}, + {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c57e493a0faea1e4c38f860d6862ba6832723396c884fbf938ff5e9b224200e2"}, + {file = "pydantic_core-2.23.2.tar.gz", hash = "sha256:95d6bf449a1ac81de562d65d180af5d8c19672793c81877a2eda8fde5d08f2fd"}, ] [package.dependencies] @@ -501,6 +502,17 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + [[package]] name = "urllib3" version = "2.2.2" From 4fb2b200573c599dff38ecacdd1956586b92c974 Mon Sep 17 00:00:00 2001 From: Denis Rozhnovskiy Date: Mon, 23 Sep 2024 22:02:46 +0500 Subject: [PATCH 04/24] deps: update pydantic update --- poetry.lock | 226 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 110 insertions(+), 118 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8dd72a4..36adfe5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -235,15 +235,18 @@ test = ["pytest (>=6)"] [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -283,123 +286,123 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "2.9.0" +version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.9.0-py3-none-any.whl", hash = "sha256:f66a7073abd93214a20c5f7b32d56843137a7a2e70d02111f3be287035c45370"}, - {file = "pydantic-2.9.0.tar.gz", hash = "sha256:c7a8a9fdf7d100afa49647eae340e2d23efa382466a8d177efcd1381e9be5598"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.23.2" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" typing-extensions = [ {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, ] -tzdata = {version = "*", markers = "python_version >= \"3.9\""} [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.23.2" +version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.23.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7d0324a35ab436c9d768753cbc3c47a865a2cbc0757066cb864747baa61f6ece"}, - {file = "pydantic_core-2.23.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:276ae78153a94b664e700ac362587c73b84399bd1145e135287513442e7dfbc7"}, - {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c7aa318da542cdcc60d4a648377ffe1a2ef0eb1e996026c7f74507b720a78"}, - {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1cf842265a3a820ebc6388b963ead065f5ce8f2068ac4e1c713ef77a67b71f7c"}, - {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae90b9e50fe1bd115b24785e962b51130340408156d34d67b5f8f3fa6540938e"}, - {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ae65fdfb8a841556b52935dfd4c3f79132dc5253b12c0061b96415208f4d622"}, - {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c8aa40f6ca803f95b1c1c5aeaee6237b9e879e4dfb46ad713229a63651a95fb"}, - {file = "pydantic_core-2.23.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c53100c8ee5a1e102766abde2158077d8c374bee0639201f11d3032e3555dfbc"}, - {file = "pydantic_core-2.23.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6b9dd6aa03c812017411734e496c44fef29b43dba1e3dd1fa7361bbacfc1354"}, - {file = "pydantic_core-2.23.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b18cf68255a476b927910c6873d9ed00da692bb293c5b10b282bd48a0afe3ae2"}, - {file = "pydantic_core-2.23.2-cp310-none-win32.whl", hash = "sha256:e460475719721d59cd54a350c1f71c797c763212c836bf48585478c5514d2854"}, - {file = "pydantic_core-2.23.2-cp310-none-win_amd64.whl", hash = "sha256:5f3cf3721eaf8741cffaf092487f1ca80831202ce91672776b02b875580e174a"}, - {file = "pydantic_core-2.23.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7ce8e26b86a91e305858e018afc7a6e932f17428b1eaa60154bd1f7ee888b5f8"}, - {file = "pydantic_core-2.23.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e9b24cca4037a561422bf5dc52b38d390fb61f7bfff64053ce1b72f6938e6b2"}, - {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753294d42fb072aa1775bfe1a2ba1012427376718fa4c72de52005a3d2a22178"}, - {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:257d6a410a0d8aeb50b4283dea39bb79b14303e0fab0f2b9d617701331ed1515"}, - {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8319e0bd6a7b45ad76166cc3d5d6a36c97d0c82a196f478c3ee5346566eebfd"}, - {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a05c0240f6c711eb381ac392de987ee974fa9336071fb697768dfdb151345ce"}, - {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d5b0ff3218858859910295df6953d7bafac3a48d5cd18f4e3ed9999efd2245f"}, - {file = "pydantic_core-2.23.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96ef39add33ff58cd4c112cbac076726b96b98bb8f1e7f7595288dcfb2f10b57"}, - {file = "pydantic_core-2.23.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0102e49ac7d2df3379ef8d658d3bc59d3d769b0bdb17da189b75efa861fc07b4"}, - {file = "pydantic_core-2.23.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6612c2a844043e4d10a8324c54cdff0042c558eef30bd705770793d70b224aa"}, - {file = "pydantic_core-2.23.2-cp311-none-win32.whl", hash = "sha256:caffda619099cfd4f63d48462f6aadbecee3ad9603b4b88b60cb821c1b258576"}, - {file = "pydantic_core-2.23.2-cp311-none-win_amd64.whl", hash = "sha256:6f80fba4af0cb1d2344869d56430e304a51396b70d46b91a55ed4959993c0589"}, - {file = "pydantic_core-2.23.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c83c64d05ffbbe12d4e8498ab72bdb05bcc1026340a4a597dc647a13c1605ec"}, - {file = "pydantic_core-2.23.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6294907eaaccf71c076abdd1c7954e272efa39bb043161b4b8aa1cd76a16ce43"}, - {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a801c5e1e13272e0909c520708122496647d1279d252c9e6e07dac216accc41"}, - {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc0c316fba3ce72ac3ab7902a888b9dc4979162d320823679da270c2d9ad0cad"}, - {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b06c5d4e8701ac2ba99a2ef835e4e1b187d41095a9c619c5b185c9068ed2a49"}, - {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82764c0bd697159fe9947ad59b6db6d7329e88505c8f98990eb07e84cc0a5d81"}, - {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b1a195efd347ede8bcf723e932300292eb13a9d2a3c1f84eb8f37cbbc905b7f"}, - {file = "pydantic_core-2.23.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7efb12e5071ad8d5b547487bdad489fbd4a5a35a0fc36a1941517a6ad7f23e0"}, - {file = "pydantic_core-2.23.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5dd0ec5f514ed40e49bf961d49cf1bc2c72e9b50f29a163b2cc9030c6742aa73"}, - {file = "pydantic_core-2.23.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:820f6ee5c06bc868335e3b6e42d7ef41f50dfb3ea32fbd523ab679d10d8741c0"}, - {file = "pydantic_core-2.23.2-cp312-none-win32.whl", hash = "sha256:3713dc093d5048bfaedbba7a8dbc53e74c44a140d45ede020dc347dda18daf3f"}, - {file = "pydantic_core-2.23.2-cp312-none-win_amd64.whl", hash = "sha256:e1895e949f8849bc2757c0dbac28422a04be031204df46a56ab34bcf98507342"}, - {file = "pydantic_core-2.23.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:da43cbe593e3c87d07108d0ebd73771dc414488f1f91ed2e204b0370b94b37ac"}, - {file = "pydantic_core-2.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:64d094ea1aa97c6ded4748d40886076a931a8bf6f61b6e43e4a1041769c39dd2"}, - {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:084414ffe9a85a52940b49631321d636dadf3576c30259607b75516d131fecd0"}, - {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043ef8469f72609c4c3a5e06a07a1f713d53df4d53112c6d49207c0bd3c3bd9b"}, - {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3649bd3ae6a8ebea7dc381afb7f3c6db237fc7cebd05c8ac36ca8a4187b03b30"}, - {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6db09153d8438425e98cdc9a289c5fade04a5d2128faff8f227c459da21b9703"}, - {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5668b3173bb0b2e65020b60d83f5910a7224027232c9f5dc05a71a1deac9f960"}, - {file = "pydantic_core-2.23.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c7b81beaf7c7ebde978377dc53679c6cba0e946426fc7ade54251dfe24a7604"}, - {file = "pydantic_core-2.23.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ae579143826c6f05a361d9546446c432a165ecf1c0b720bbfd81152645cb897d"}, - {file = "pydantic_core-2.23.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:19f1352fe4b248cae22a89268720fc74e83f008057a652894f08fa931e77dced"}, - {file = "pydantic_core-2.23.2-cp313-none-win32.whl", hash = "sha256:e1a79ad49f346aa1a2921f31e8dbbab4d64484823e813a002679eaa46cba39e1"}, - {file = "pydantic_core-2.23.2-cp313-none-win_amd64.whl", hash = "sha256:582871902e1902b3c8e9b2c347f32a792a07094110c1bca6c2ea89b90150caac"}, - {file = "pydantic_core-2.23.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:743e5811b0c377eb830150d675b0847a74a44d4ad5ab8845923d5b3a756d8100"}, - {file = "pydantic_core-2.23.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6650a7bbe17a2717167e3e23c186849bae5cef35d38949549f1c116031b2b3aa"}, - {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56e6a12ec8d7679f41b3750ffa426d22b44ef97be226a9bab00a03365f217b2b"}, - {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810ca06cca91de9107718dc83d9ac4d2e86efd6c02cba49a190abcaf33fb0472"}, - {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:785e7f517ebb9890813d31cb5d328fa5eda825bb205065cde760b3150e4de1f7"}, - {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ef71ec876fcc4d3bbf2ae81961959e8d62f8d74a83d116668409c224012e3af"}, - {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d50ac34835c6a4a0d456b5db559b82047403c4317b3bc73b3455fefdbdc54b0a"}, - {file = "pydantic_core-2.23.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16b25a4a120a2bb7dab51b81e3d9f3cde4f9a4456566c403ed29ac81bf49744f"}, - {file = "pydantic_core-2.23.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:41ae8537ad371ec018e3c5da0eb3f3e40ee1011eb9be1da7f965357c4623c501"}, - {file = "pydantic_core-2.23.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07049ec9306ec64e955b2e7c40c8d77dd78ea89adb97a2013d0b6e055c5ee4c5"}, - {file = "pydantic_core-2.23.2-cp38-none-win32.whl", hash = "sha256:086c5db95157dc84c63ff9d96ebb8856f47ce113c86b61065a066f8efbe80acf"}, - {file = "pydantic_core-2.23.2-cp38-none-win_amd64.whl", hash = "sha256:67b6655311b00581914aba481729971b88bb8bc7996206590700a3ac85e457b8"}, - {file = "pydantic_core-2.23.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:358331e21a897151e54d58e08d0219acf98ebb14c567267a87e971f3d2a3be59"}, - {file = "pydantic_core-2.23.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c4d9f15ffe68bcd3898b0ad7233af01b15c57d91cd1667f8d868e0eacbfe3f87"}, - {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0123655fedacf035ab10c23450163c2f65a4174f2bb034b188240a6cf06bb123"}, - {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e6e3ccebdbd6e53474b0bb7ab8b88e83c0cfe91484b25e058e581348ee5a01a5"}, - {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc535cb898ef88333cf317777ecdfe0faac1c2a3187ef7eb061b6f7ecf7e6bae"}, - {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aab9e522efff3993a9e98ab14263d4e20211e62da088298089a03056980a3e69"}, - {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05b366fb8fe3d8683b11ac35fa08947d7b92be78ec64e3277d03bd7f9b7cda79"}, - {file = "pydantic_core-2.23.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7568f682c06f10f30ef643a1e8eec4afeecdafde5c4af1b574c6df079e96f96c"}, - {file = "pydantic_core-2.23.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cdd02a08205dc90238669f082747612cb3c82bd2c717adc60f9b9ecadb540f80"}, - {file = "pydantic_core-2.23.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a2ab4f410f4b886de53b6bddf5dd6f337915a29dd9f22f20f3099659536b2f6"}, - {file = "pydantic_core-2.23.2-cp39-none-win32.whl", hash = "sha256:0448b81c3dfcde439551bb04a9f41d7627f676b12701865c8a2574bcea034437"}, - {file = "pydantic_core-2.23.2-cp39-none-win_amd64.whl", hash = "sha256:4cebb9794f67266d65e7e4cbe5dcf063e29fc7b81c79dc9475bd476d9534150e"}, - {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e758d271ed0286d146cf7c04c539a5169a888dd0b57026be621547e756af55bc"}, - {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f477d26183e94eaafc60b983ab25af2a809a1b48ce4debb57b343f671b7a90b6"}, - {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da3131ef2b940b99106f29dfbc30d9505643f766704e14c5d5e504e6a480c35e"}, - {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329a721253c7e4cbd7aad4a377745fbcc0607f9d72a3cc2102dd40519be75ed2"}, - {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7706e15cdbf42f8fab1e6425247dfa98f4a6f8c63746c995d6a2017f78e619ae"}, - {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e64ffaf8f6e17ca15eb48344d86a7a741454526f3a3fa56bc493ad9d7ec63936"}, - {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dd59638025160056687d598b054b64a79183f8065eae0d3f5ca523cde9943940"}, - {file = "pydantic_core-2.23.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:12625e69b1199e94b0ae1c9a95d000484ce9f0182f9965a26572f054b1537e44"}, - {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d813fd871b3d5c3005157622ee102e8908ad6011ec915a18bd8fde673c4360e"}, - {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1eb37f7d6a8001c0f86dc8ff2ee8d08291a536d76e49e78cda8587bb54d8b329"}, - {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ce7eaf9a98680b4312b7cebcdd9352531c43db00fca586115845df388f3c465"}, - {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f087879f1ffde024dd2788a30d55acd67959dcf6c431e9d3682d1c491a0eb474"}, - {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ce883906810b4c3bd90e0ada1f9e808d9ecf1c5f0b60c6b8831d6100bcc7dd6"}, - {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a8031074a397a5925d06b590121f8339d34a5a74cfe6970f8a1124eb8b83f4ac"}, - {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23af245b8f2f4ee9e2c99cb3f93d0e22fb5c16df3f2f643f5a8da5caff12a653"}, - {file = "pydantic_core-2.23.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c57e493a0faea1e4c38f860d6862ba6832723396c884fbf938ff5e9b224200e2"}, - {file = "pydantic_core-2.23.2.tar.gz", hash = "sha256:95d6bf449a1ac81de562d65d180af5d8c19672793c81877a2eda8fde5d08f2fd"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] [package.dependencies] @@ -407,13 +410,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -502,26 +505,15 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -[[package]] -name = "tzdata" -version = "2024.1" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -533,4 +525,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.10" -content-hash = "f19125c79d2d577a626ea1de0922e09f9614ab609e313f5a5f98e67a14a454e2" +content-hash = "89ecb374a8fe94def651cc68e2511cf208f2e047b86151511f6bed21fc13cde2" diff --git a/pyproject.toml b/pyproject.toml index d5445e8..62e8a24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.10" -pydantic = "^2.0.0" +pydantic = "^2.9.2" requests = "^2.32.3" requests-toolbelt = "^1.0.0" From 617feccccdce0c6eeb3dc51c7d74a9c5ec953ca4 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Thu, 9 Jan 2025 22:05:30 +0500 Subject: [PATCH 05/24] feat: Now fully async - Async-First Design: Built with modern async/await patterns for optimal performance - Type Safety: Full typing support with runtime validation via Pydantic - Comprehensive API Coverage: Support for all Outline VPN Server API endpoints - Error Handling: Robust error handling with custom exception types - SSL/TLS Security: Certificate fingerprint verification for enhanced security - Flexible Response Format: Choose between Pydantic models or JSON responses - Data Transfer Metrics: Built-in support for monitoring server and key usage - Rate Limiting: Built-in handling of API rate limits - Context Manager Support: Clean resource management with async context managers --- README.md | 195 ++++-- poetry.lock | 1328 +++++++++++++++++++++++++++--------- pyoutlineapi/__init__.py | 31 + pyoutlineapi/client.py | 639 +++++++++++++---- pyoutlineapi/exceptions.py | 46 -- pyoutlineapi/logger.py | 37 - pyoutlineapi/models.py | 201 +++--- pyproject.toml | 25 +- tests/test_client.py | 133 ---- tests/test_exceptions.py | 43 -- tests/test_logger.py | 61 -- tests/test_models.py | 84 --- 12 files changed, 1773 insertions(+), 1050 deletions(-) create mode 100644 pyoutlineapi/__init__.py delete mode 100644 pyoutlineapi/exceptions.py delete mode 100644 pyoutlineapi/logger.py delete mode 100644 tests/test_client.py delete mode 100644 tests/test_exceptions.py delete mode 100644 tests/test_logger.py delete mode 100644 tests/test_models.py diff --git a/README.md b/README.md index 33d5ce9..66c9335 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # PyOutlineAPI -`pyoutlineapi` is a Python package designed to interact with the Outline VPN Server API, providing robust data -validation through Pydantic models. This ensures reliable and secure API interactions, making it an excellent choice for -integrating with bots and other automated systems that require accurate and secure communication. +A modern, async-first Python client for the Outline VPN Server API with comprehensive data validation through Pydantic +models. [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) @@ -12,119 +11,171 @@ integrating with bots and other automated systems that require accurate and secu ## Features -- **Server Management**: Retrieve server information, update hostnames, manage ports, and more. -- **Access Key Management**: Create, list, rename, and delete access keys, as well as set data limits. -- **Metrics**: Enable or disable metrics sharing and retrieve data transfer metrics. -- **Experimental Endpoints**: Access and manage experimental features of the Outline Server API. - -## Quick Start - -To get started with `pyoutlineapi`, follow these steps: - -1. Install the package using pip or Poetry. -2. Initialize the `PyOutlineWrapper` client with your Outline VPN server URL and certificate fingerprint. -3. Use the provided methods to interact with the server and access keys. - -See the examples below for more detailed information. +- **Async-First Design**: Built with modern async/await patterns for optimal performance +- **Type Safety**: Full typing support with runtime validation via Pydantic +- **Comprehensive API Coverage**: Support for all Outline VPN Server + API [endpoints](https://github.com/Jigsaw-Code/outline-server/blob/master/src/shadowbox/server/api.yml) +- **Error Handling**: Robust error handling with custom exception types +- **SSL/TLS Security**: Certificate fingerprint verification for enhanced security +- **Flexible Response Format**: Choose between Pydantic models or JSON responses +- **Data Transfer Metrics**: Built-in support for monitoring server and key usage +- **Rate Limiting**: Built-in handling of API rate limits +- **Context Manager Support**: Clean resource management with async context managers ## Installation -You can install PyOutlineAPI via [PyPI](https://pypi.org/project/pyoutlineapi/) using pip: +Install via pip: ```bash pip install pyoutlineapi ``` -Or via [Poetry](https://python-poetry.org/): +Or using Poetry: ```bash poetry add pyoutlineapi ``` -## Basic Operations +## Quick Start -### Initialize the Client +Here's a simple example to get you started: ```python -from pyoutlineapi.client import PyOutlineWrapper -from pyoutlineapi.models import DataLimit - -# Initialize the API client -api_url = "https://your-outline-url.com" -cert_sha256 = "your-cert-sha256-fingerprint" -# Set "verify_tls" to False if using a self-signed certificate. -# Set "json_format" to True if answers need to be returned in JSON format. Defaults to False - Pydantic models will be returned. -api_client = PyOutlineWrapper(api_url=api_url, cert_sha256=cert_sha256, verify_tls=False, json_format=True) -``` +import asyncio +from pyoutlineapi import AsyncOutlineClient -### Retrieve Server Information -```python -server_info = api_client.get_server_info() -print("Server Information:", server_info) -``` +async def main(): + async with AsyncOutlineClient( + api_url="https://your-outline-server:port/api", + cert_sha256="your-certificate-fingerprint" + ) as client: + # Get server info + server = await client.get_server_info() + print(f"Connected to {server.name} running version {server.version}") -### Create a New Access Key + # Create a new access key + key = await client.create_access_key(name="TestUser") + print(f"Created key: {key.access_url}") -```python -# Create a new access key with default values -new_access_key = api_client.create_access_key() -# Create a new access key with custom values -new_access_key = api_client.create_access_key(name="my_access_key", password="secure_password", port=8080) -print("New Access Key:", new_access_key) +if __name__ == "__main__": + asyncio.run(main()) ``` -### Delete Access Key +## Detailed Usage + +### Client Configuration + +The client can be configured with several options: ```python -success = api_client.delete_access_key("example-key-id") -print("Access Key Deleted Successfully" if success else "Failed to Delete Access Key") +from pyoutlineapi import AsyncOutlineClient + +client = AsyncOutlineClient( + api_url="https://your-outline-server:port/api", + cert_sha256="your-certificate-fingerprint", + json_format=True, # Return JSON instead of Pydantic models + timeout=30.0 # Request timeout in seconds +) ``` -## Additional Functions +### Managing Access Keys -### Update Server Port +Create and manage access keys: ```python -update_success = api_client.update_server_port(9090) -print("Server Port Updated:", update_success) +async def manage_keys(): + async with AsyncOutlineClient(...) as client: + # Create a key with data limit + key = await client.create_access_key( + name="Limited User", + port=8388, + limit=DataLimit(bytes=5 * 1024 ** 3) # 5 GB limit + ) + + # List all keys + keys = await client.get_access_keys() + for key in keys.access_keys: + print(f"Key {key.id}: {key.name or 'unnamed'}") + + # Modify a key + await client.rename_access_key(1, "New Name") + await client.set_access_key_data_limit(1, 10 * 1024 ** 3) # 10 GB + + # Delete a key + await client.delete_access_key(1) ``` -### Set Data Limit for an Access Key +### Server Management + +Configure server settings: ```python -data_limit = api_client.set_access_key_data_limit("example-key-id", DataLimit(bytes=50000000)) -print("Data Limit Set:", data_limit) +async def configure_server(): + async with AsyncOutlineClient(...) as client: + # Update server name + await client.rename_server("My VPN Server") + + # Set hostname for access keys + await client.set_hostname("vpn.example.com") + + # Configure default port for new keys + await client.set_default_port(8388) ``` -### Retrieve Metrics +### Metrics Collection + +Monitor server usage: ```python -metrics_data = api_client.get_metrics() -print("Metrics Data:", metrics_data) +from pyoutlineapi.models import MetricsPeriod + + +async def get_metrics(): + async with AsyncOutlineClient(...) as client: + # Enable metrics collection + await client.set_metrics_status(True) + + # Get transfer metrics + metrics = await client.get_transfer_metrics(MetricsPeriod.MONTHLY) + for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items(): + print(f"User {user_id}: {bytes_transferred / 1024 ** 3:.2f} GB") + + # Get detailed metrics + detailed = await client.get_experimental_metrics() + for server in detailed.server: + print(f"Location: {server.location}") + print(f"Data: {server.data_transferred.bytes / 1024 ** 2:.2f} MB") ``` -## Contributing +## Error Handling -We welcome contributions to PyOutlineAPI! Please follow the guidelines outlined in -the [CONTRIBUTING.md](https://github.com/orenlab/pyoutlineapi/blob/main/CONTRIBUTING.md) file. +The client provides custom exceptions for different error scenarios: -## License +```python +from pyoutlineapi import OutlineError, APIError -PyOutlineAPI is licensed under the MIT [License](https://github.com/orenlab/pyoutlineapi/blob/main/LICENSE). See the -LICENSE file for more details. -## Frequently Asked Questions (FAQ) +async def handle_errors(): + try: + async with AsyncOutlineClient(...) as client: + await client.get_server_info() + except APIError as e: + print(f"API error: {e}") + except OutlineError as e: + print(f"Client error: {e}") +``` + +## Contributing -___ -**How to use self-signed certificates?** +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on how to submit pull +requests, report issues, and contribute to the project. -Set the `verify_tls` parameter to `False` when initializing the client. -___ -___ -**How to change the response format to Pydantic models?** +## Security -Set the `json_format` parameter to `False` when initializing the client if you need to receive responses in Pydantic -models. -___ \ No newline at end of file +If you discover any security-related issues, please email security@example.com instead of using the issue tracker. + +## License + +PyOutlineAPI is open-sourced software licensed under the [MIT license](LICENSE). \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 36adfe5..eeeab85 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,130 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.11" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, + {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, + {file = "aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c"}, + {file = "aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745"}, + {file = "aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773"}, + {file = "aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62"}, + {file = "aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e"}, + {file = "aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600"}, + {file = "aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5"}, + {file = "aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d"}, + {file = "aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99"}, + {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e"}, + {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add"}, + {file = "aiohttp-3.11.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e"}, + {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28"}, + {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226"}, + {file = "aiohttp-3.11.11-cp39-cp39-win32.whl", hash = "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3"}, + {file = "aiohttp-3.11.11-cp39-cp39-win_amd64.whl", hash = "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1"}, + {file = "aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.2" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" [[package]] name = "annotated-types" @@ -6,127 +132,115 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] -name = "certifi" -version = "2024.8.30" -description = "Python package for providing Mozilla's CA Bundle." +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.11\"" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] +[[package]] +name = "attrs" +version = "24.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -134,83 +248,74 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.10" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] [package.dependencies] @@ -225,6 +330,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -233,12 +340,132 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +groups = ["dev"] +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -253,28 +480,249 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy" +version = "1.14.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -284,24 +732,126 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "propcache" +version = "0.2.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"}, + {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"}, + {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + [[package]] name = "pydantic" -version = "2.9.2" +version = "2.10.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, - {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, + {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, + {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.23.4" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -309,114 +859,139 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.23.4" +version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, - {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, - {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, - {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, - {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, - {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, - {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, - {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, - {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, - {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, - {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, - {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, - {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, - {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, ] [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -436,6 +1011,7 @@ version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, @@ -448,50 +1024,47 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -description = "A utility belt for advanced users of python-requests" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, - {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, -] - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -500,29 +1073,110 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] -name = "urllib3" -version = "2.2.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "yarl" +version = "1.18.3" +description = "Yet another URL library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, + {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, + {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, ] -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" [metadata] -lock-version = "2.0" -python-versions = ">=3.10" -content-hash = "89ecb374a8fe94def651cc68e2511cf208f2e047b86151511f6bed21fc13cde2" +lock-version = "2.1" +python-versions = ">=3.10,<4.0" +content-hash = "1cda916d7c46267e462caef25d7dab0bc7b4044c2a0de2efc06f2f50f9ee2470" diff --git a/pyoutlineapi/__init__.py b/pyoutlineapi/__init__.py new file mode 100644 index 0000000..6009ca9 --- /dev/null +++ b/pyoutlineapi/__init__.py @@ -0,0 +1,31 @@ +from .client import AsyncOutlineClient, OutlineError, APIError +from .models import ( + AccessKey, + AccessKeyCreateRequest, + AccessKeyList, + DataLimit, + ErrorResponse, + ExperimentalMetrics, + MetricsPeriod, + MetricsStatusResponse, + Server, + ServerMetrics, +) + +__version__ = "0.2.0" + +__all__ = [ + "AsyncOutlineClient", + "OutlineError", + "APIError", + "AccessKey", + "AccessKeyCreateRequest", + "AccessKeyList", + "DataLimit", + "ErrorResponse", + "ExperimentalMetrics", + "MetricsPeriod", + "MetricsStatusResponse", + "Server", + "ServerMetrics", +] diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index c29ea7c..14b5f17 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -1,211 +1,598 @@ -from typing import Union, Optional, Type +from __future__ import annotations -import requests -from pydantic import BaseModel, ValidationError -from requests_toolbelt.adapters.fingerprint import FingerprintAdapter +import binascii +from typing import Any, Literal, TypeAlias, Union, overload, Optional +from urllib.parse import urlparse -from pyoutlineapi.exceptions import APIError -from pyoutlineapi.models import DataLimit, ServerPort, Metrics, AccessKeyList, AccessKey, AccessKeyCreateRequest, Server +import aiohttp +from aiohttp import ClientResponse, Fingerprint +from pydantic import BaseModel +from .models import ( + AccessKey, + AccessKeyCreateRequest, + AccessKeyList, + DataLimit, + ErrorResponse, + MetricsPeriod, + MetricsStatusResponse, + Server, + ServerMetrics, +) -class PyOutlineWrapper: - """ - Class for interacting with the Outline VPN Server API. - This class provides methods to interact with the Outline VPN Server, including: +class OutlineError(Exception): + """Base exception for Outline client errors.""" - - Retrieving server information - - Creating, listing, and deleting access keys - - Updating server ports - - Setting and removing data limits for access keys - - Retrieving metrics - The class uses the `requests` library for making HTTP requests and `pydantic` for data validation. - Responses can be returned either as Pydantic models or in JSON format, depending on the `json_format` parameter. - """ +class APIError(OutlineError): + """Raised when API requests fail.""" - def __init__(self, api_url: str, cert_sha256: str, verify_tls: bool = True, json_format: bool = True): - """ - Initializes the PyOutlineWrapper with the given API URL, certificate fingerprint, and options for TLS verification - and response format. - Args: - api_url (str): The base URL of the Outline VPN Server API. - cert_sha256 (str): The SHA-256 fingerprint of the server's certificate. - verify_tls (bool, optional): Whether to verify the server's TLS certificate. Defaults to True. - json_format (bool, optional): Whether to return responses in JSON format. Defaults to True. - """ - self._api_url = api_url +# Type aliases +JsonDict: TypeAlias = dict[str, Any] + + +class AsyncOutlineClient: + """ + Asynchronous client for the Outline VPN Server API. + + Args: + api_url: Base URL for the Outline server API + cert_sha256: SHA-256 fingerprint of the server's TLS certificate + json_format: Return raw JSON instead of Pydantic models + timeout: Request timeout in seconds + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... server_info = await client.get_server_info() + """ + + def __init__( + self, + api_url: str, + cert_sha256: str, + *, + json_format: bool = True, + timeout: float = 30.0, + ) -> None: + self._api_url = api_url.rstrip("/") self._cert_sha256 = cert_sha256 - self._verify_tls = verify_tls self._json_format = json_format - self._session = requests.Session() - self._session.mount(self._api_url, FingerprintAdapter(self._cert_sha256)) - - def _request(self, method: str, endpoint: str, json_data=None) -> requests.Response: + self._timeout = aiohttp.ClientTimeout(total=timeout) + self._ssl_context = None + self._session: Optional[aiohttp.ClientSession] = None + self._in_context = False + + async def __aenter__(self) -> AsyncOutlineClient: + """Set up client session for context manager.""" + self._session = aiohttp.ClientSession( + timeout=self._timeout, raise_for_status=True + ) + self._in_context = True + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Clean up client session.""" + if self._session: + await self._session.close() + self._session = None + self._in_context = False + + def _ensure_context(self): + """Ensure the session context is valid.""" + if not self._session or self._session.closed: + raise RuntimeError("Client session is not initialized or already closed.") + + @overload + async def _parse_response( + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[True], + ) -> JsonDict: + ... + + @overload + async def _parse_response( + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[False], + ) -> BaseModel: + ... + + @overload + async def _parse_response( + self, response: ClientResponse, model: type[BaseModel], json_format: bool + ) -> Union[JsonDict, BaseModel]: + ... + + async def _parse_response( + self, response: ClientResponse, model: type[BaseModel], json_format: bool = True + ) -> Union[JsonDict, BaseModel]: """ - Makes an HTTP request to the API. + Parse and validate API response data. Args: - method (str): The HTTP method to use (e.g., 'GET', 'POST', 'PUT', 'DELETE'). - endpoint (str): The API endpoint to call. - json_data (optional): The JSON data to send with the request. + response: API response to parse + model: Pydantic model for validation + json_format: Whether to return raw JSON Returns: - requests.Response: The HTTP response object. + Validated response data Raises: - APIError: If the request fails or the response status is not successful. + ValueError: If response validation fails """ - url = f"{self._api_url}/{endpoint}" + self._ensure_context() + try: - response = self._session.request( + data = await response.json() + except aiohttp.ContentTypeError: + raise ValueError("Invalid response format") from None + try: + validated = model.model_validate(data) + return validated.model_dump() if json_format else validated + except Exception as e: + raise ValueError(f"Value error: {e}") from e + + @staticmethod + async def _handle_error_response(response: ClientResponse) -> None: + """Handle error responses from the API.""" + try: + error_data = await response.json() + error = ErrorResponse.model_validate(error_data) + raise APIError(f"{error.code}: {error.message}") + except ValueError: + raise APIError(f"HTTP {response.status}: {response.reason}") + + async def _request( + self, + method: str, + endpoint: str, + *, + json: Any = None, + params: Optional[dict[str, Any]] = None, + ) -> Any: + """Make an API request.""" + self._ensure_context() + + url = self._build_url(endpoint) + ssl_context = self._get_ssl_context() + + async with self._session.request( method, url, - json=json_data, - verify=self._verify_tls, - timeout=15 - ) - response.raise_for_status() - return response - except requests.RequestException as exception: - raise APIError(f"Request to {url} failed: {exception}") + json=json, + params=params, + ssl=ssl_context, + raise_for_status=False, + timeout=self._timeout, + ) as response: + if response.status >= 400: + await self._handle_error_response(response) + + if response.status == 204: + return True # No content response + + try: + await response.json() + return response + except aiohttp.ContentTypeError: + return await response.text() # Fallback for non-JSON responses + except Exception as e: + raise APIError(f"Failed to parse response from {url}: {e}") from e + + def _build_url(self, endpoint: str) -> str: + """Build and validate the full URL for the API request.""" + if not isinstance(endpoint, str): + raise ValueError("Endpoint must be a string") + + endpoint = endpoint.lstrip("/") + url = f"{self._api_url}/{endpoint}" + + parsed_url = urlparse(url) + if not parsed_url.scheme or not parsed_url.netloc: + raise ValueError(f"Invalid URL: {url}") - def _parse_response(self, response: requests.Response, model: Type[BaseModel]) -> Union[BaseModel, str]: + return url + + def _get_ssl_context(self) -> Optional[Fingerprint]: + """Create an SSL context if a certificate fingerprint is provided.""" + if not self._cert_sha256: + return None + + try: + fingerprint = binascii.unhexlify(self._cert_sha256) + return Fingerprint(fingerprint) + except binascii.Error as e: + raise ValueError(f"Invalid certificate SHA256: {self._cert_sha256}") from e + except Exception as e: + raise OutlineError("Error while creating SSL context") from e + + async def get_server_info(self) -> Union[JsonDict, Server]: """ - Parses the response from the API. + Get server information. + + Returns: + Server information including name, ID, and configuration. + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... server = await client.get_server_info() + ... print(f"Server {server.name} running version {server.version}") + """ + response = await self._request("GET", "server") + return await self._parse_response( + response, Server, json_format=self._json_format + ) + + async def rename_server(self, name: str) -> bool: + """ + Rename the server. Args: - response (requests.Response): The HTTP response object. - model (Type[BaseModel]): The Pydantic model to validate the response data. + name: New server name Returns: - Union[BaseModel, str]: The validated data as a Pydantic model or a JSON string, depending on the json_format parameter. - - Raises: - ValidationError: If the response data does not match the Pydantic model. + True if successful + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... success = await client.rename_server("My VPN Server") + ... if success: + ... print("Server renamed successfully") """ - try: - json_data = response.json() - data = model.model_validate(json_data) - return data.model_dump_json() if self._json_format else data - except ValidationError as e: - raise ValidationError(f"Validation error: {e}") + return await self._request("PUT", "name", json={"name": name}) - def get_server_info(self) -> Union[Server, str]: + async def set_hostname(self, hostname: str) -> bool: """ - Retrieves information about the Outline VPN server. + Set server hostname for access keys. + + Args: + hostname: New hostname or IP address Returns: - Union[Server, str]: The server information as a Pydantic model or a JSON string. + True if successful + + Raises: + APIError: If hostname is invalid + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... await client.set_hostname("vpn.example.com") + ... # Or use IP address + ... await client.set_hostname("203.0.113.1") """ - response = self._request("GET", "server") - return self._parse_response(response, Server) + return await self._request( + "PUT", "server/hostname-for-access-keys", json={"hostname": hostname} + ) - def create_access_key(self, name: Optional[str] = None, password: Optional[str] = None, - port: Optional[int] = None) -> Union[AccessKey, str]: + async def set_default_port(self, port: int) -> bool: """ - Creates a new access key. + Set default port for new access keys. Args: - name (Optional[str]): The name of the access key. - password (Optional[str]): The password for the access key. - port (Optional[int]): The port for the access key. + port: Port number (1025-65535) Returns: - Union[AccessKey, str]: The created access key as a Pydantic model or a JSON string. + True if successful + + Raises: + APIError: If port is invalid or in use + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... await client.set_default_port(8388) + """ - request_data = { - "name": name, - "password": password, - "port": port, - } - request_data = {key: value for key, value in request_data.items() if value is not None} + return await self._request( + "PUT", "server/port-for-new-access-keys", json={"port": port} + ) - if request_data: - request_data = AccessKeyCreateRequest(**request_data).model_dump() + async def get_metrics_status(self) -> dict[str, Any] | BaseModel: + """ + Get whether metrics collection is enabled. - response = self._request("POST", "access-keys", json_data=request_data) - return self._parse_response(response, AccessKey) + Returns: + Current metrics collection status + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... if await client.get_metrics_status(): + ... print("Metrics collection is enabled") + """ + response = await self._request("GET", "metrics/enabled") + data = await self._parse_response( + response, MetricsStatusResponse, json_format=self._json_format + ) + return data - def get_access_keys(self) -> Union[AccessKeyList, str]: + async def set_metrics_status(self, enabled: bool) -> bool: """ - Retrieves a list of all access keys. + Enable or disable metrics collection. + + Args: + enabled: Whether to enable metrics Returns: - Union[AccessKeyList, str]: The list of access keys as a Pydantic model or a JSON string. + True if successful + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Enable metrics + ... await client.set_metrics_status(True) + ... # Check new status + ... is_enabled = await client.get_metrics_status() """ - response = self._request("GET", "access-keys") - return self._parse_response(response, AccessKeyList) + return await self._request( + "PUT", "metrics/enabled", json={"metricsEnabled": enabled} + ) - def delete_access_key(self, key_id: str) -> bool: + async def get_transfer_metrics( + self, period: MetricsPeriod = MetricsPeriod.MONTHLY + ) -> Union[JsonDict, ServerMetrics]: """ - Deletes an access key by its ID. + Get transfer metrics for specified period. Args: - key_id (str): The ID of the access key to delete. + period: Time period for metrics (DAILY, WEEKLY, or MONTHLY) + + Returns: + Transfer metrics data for each access key + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Get monthly metrics + ... metrics = await client.get_transfer_metrics() + ... # Or get daily metrics + ... daily = await client.get_transfer_metrics(MetricsPeriod.DAILY) + ... for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items(): + ... print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB") + """ + response = await self._request( + "GET", "metrics/transfer", params={"period": period.value} + ) + return await self._parse_response( + response, ServerMetrics, json_format=self._json_format + ) + + async def create_access_key( + self, + *, + name: Optional[str] = None, + password: Optional[str] = None, + port: Optional[int] = None, + method: Optional[str] = None, + limit: Optional[DataLimit] = None, + ) -> Union[JsonDict, AccessKey]: + """ + Create a new access key. + + Args: + name: Optional key name + password: Optional password + port: Optional port number (1-65535) + method: Optional encryption method + limit: Optional data transfer limit + + Returns: + New access key details + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Create basic key + ... key = await client.create_access_key(name="User 1") + ... + ... # Create key with data limit + ... _limit = DataLimit(bytes=5 * 1024**3) # 5 GB + ... key = await client.create_access_key( + ... name="Limited User", + ... port=8388, + ... limit=_limit + ... ) + ... print(f"Created key: {key.access_url}") + """ + request = AccessKeyCreateRequest( + name=name, password=password, port=port, method=method, limit=limit + ) + response = await self._request( + "POST", "access-keys", json=request.model_dump(exclude_none=True) + ) + return await self._parse_response( + response, AccessKey, json_format=self._json_format + ) + + async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]: + """ + Get all access keys. Returns: - bool: True if the access key was successfully deleted, False otherwise. + List of all access keys + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... keys = await client.get_access_keys() + ... for key in keys.access_keys: + ... print(f"Key {key.id}: {key.name or 'unnamed'}") + ... if key.data_limit: + ... print(f" Limit: {key.data_limit.bytes / 1024**3:.1f} GB") """ - response = self._request("DELETE", f"access-keys/{key_id}") - return response.status_code == 204 + response = await self._request("GET", "access-keys") + return await self._parse_response( + response, AccessKeyList, json_format=self._json_format + ) - def update_server_port(self, port: int) -> bool: + async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]: """ - Updates the port for new access keys on the server. + Get specific access key. Args: - port (int): The new port number. + key_id: Access key ID Returns: - bool: True if the port was successfully updated, False otherwise. + Access key details Raises: - APIError: If the port is already in use. + APIError: If key doesn't exist + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... key = await client.get_access_key(1) + ... print(f"Port: {key.port}") + ... print(f"URL: {key.access_url}") """ - verified_port = ServerPort(port=port) - response = self._request("PUT", "server/port-for-new-access-keys", {"port": verified_port.port}) - if response.status_code == 409: - raise APIError(f"Port {verified_port.port} is already in use") - return response.status_code == 204 + response = await self._request("GET", f"access-keys/{key_id}") + return await self._parse_response( + response, AccessKey, json_format=self._json_format + ) - def set_access_key_data_limit(self, key_id: str, limit: DataLimit) -> bool: + async def rename_access_key(self, key_id: int, name: str) -> bool: """ - Sets a data limit for an access key. + Rename access key. Args: - key_id (str): The ID of the access key. - limit (DataLimit): The data limit to set. + key_id: Access key ID + name: New name Returns: - bool: True if the data limit was successfully set, False otherwise. + True if successful + + Raises: + APIError: If key doesn't exist + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Rename key + ... await client.rename_access_key(1, "Alice") + ... + ... # Verify new name + ... key = await client.get_access_key(1) + ... assert key.name == "Alice" """ - response = self._request("PUT", f"access-keys/{key_id}/data-limit", {"bytes": limit.bytes}) - return response.status_code == 204 + return await self._request( + "PUT", f"access-keys/{key_id}/name", json={"name": name} + ) - def get_metrics(self) -> Union[Metrics, str]: + async def delete_access_key(self, key_id: int) -> bool: """ - Retrieves transfer metrics from the server. + Delete access key. + + Args: + key_id: Access key ID Returns: - Union[Metrics, str]: The metrics data as a Pydantic model or a JSON string. + True if successful + + Raises: + APIError: If key doesn't exist + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... if await client.delete_access_key(1): + ... print("Key deleted") + """ - response = self._request("GET", "metrics/transfer") - return self._parse_response(response, Metrics) + return await self._request("DELETE", f"access-keys/{key_id}") - def remove_access_key_data_limit(self, key_id: str) -> bool: + async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool: """ - Removes the data limit for an access key. + Set data transfer limit for access key. Args: - key_id (str): The ID of the access key. + key_id: Access key ID + bytes_limit: Limit in bytes (must be positive) Returns: - bool: True if the data limit was successfully removed, False otherwise. + True if successful + + Raises: + APIError: If key doesn't exist or limit is invalid + + Examples: + >>> async def doo_something(): + ... async with AsyncOutlineClient( + ... "https://example.com:1234/secret", + ... "ab12cd34..." + ... ) as client: + ... # Set 5 GB limit + ... limit = 5 * 1024**3 # 5 GB in bytes + ... await client.set_access_key_data_limit(1, limit) + ... + ... # Verify limit + ... key = await client.get_access_key(1) + ... assert key.data_limit and key.data_limit.bytes == limit + """ + return await self._request( + "PUT", + f"access-keys/{key_id}/data-limit", + json={"limit": {"bytes": bytes_limit}}, + ) + + async def remove_access_key_data_limit(self, key_id: str) -> bool: """ - response = self._request("DELETE", f"access-keys/{key_id}/data-limit") - return response.status_code == 204 + Remove data transfer limit from access key. + + Args: + key_id: Access key ID + Returns: + True if successful -__all__ = ["PyOutlineWrapper"] + Raises: + APIError: If key doesn't exist + """ + return await self._request("DELETE", f"access-keys/{key_id}/data-limit") diff --git a/pyoutlineapi/exceptions.py b/pyoutlineapi/exceptions.py deleted file mode 100644 index ee2d807..0000000 --- a/pyoutlineapi/exceptions.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. - -""" - - -class APIError(Exception): - """Base class for all API-related errors.""" - - def __init__(self, message: str): - super().__init__(message) - self.message = message - - def __str__(self): - return self.message - - -class HTTPError(APIError): - """Raised for HTTP-related errors (e.g., HTTP status codes).""" - - def __init__(self, status_code: int, message: str): - self.status_code = status_code - super().__init__(message) - - def __str__(self): - return f"HTTP error occurred: {self.status_code} - {self.message}" - - -class RequestError(APIError): - """Raised for request-related errors (e.g., connection issues).""" - - def __init__(self, message: str): - super().__init__(f"An error occurred while requesting data: {message}") - - -class ValidationError(APIError): - """Raised for validation errors when processing API responses.""" - - def __init__(self, message: str): - super().__init__(f"Validation error occurred: {message}") diff --git a/pyoutlineapi/logger.py b/pyoutlineapi/logger.py deleted file mode 100644 index 8f65453..0000000 --- a/pyoutlineapi/logger.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. - -""" - -import logging - - -def setup_logger(name: str) -> logging.Logger: - """ - Set up a logger with a specified name. - - Args: - name (str): The name of the logger. - - Returns: - logging.Logger: Configured logger instance. - """ - logger = logging.getLogger(name) - logger.setLevel(logging.DEBUG) # Set the base logging level - - # Create a console handler with a specific format - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) # Set the level for the console handler - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - ch.setFormatter(formatter) - - # Add the handler to the logger - logger.addHandler(ch) - - return logger diff --git a/pyoutlineapi/models.py b/pyoutlineapi/models.py index b7ae056..5b50da0 100644 --- a/pyoutlineapi/models.py +++ b/pyoutlineapi/models.py @@ -1,139 +1,130 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy +from enum import Enum +from typing import Optional -This file is part of the PyOutlineAPI project. +from pydantic import BaseModel, Field, field_validator -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. -Licensed under the MIT License. See the LICENSE file for more details. -""" +class MetricsPeriod(str, Enum): + """Time periods for metrics collection.""" -from typing import Optional, List, Dict + DAILY = "daily" + WEEKLY = "weekly" + MONTHLY = "monthly" -from pydantic import BaseModel, Field, constr, field_validator +class DataLimit(BaseModel): + """Data transfer limit configuration.""" -class Server(BaseModel): - """ - Model for server information. - - Attributes: - name (str): The name of the server. - serverId (str): The unique identifier for the server. - metricsEnabled (bool): Indicates if metrics collection is enabled. - createdTimestampMs (int): The timestamp when the server was created, must be non-negative. - portForNewAccessKeys (int): The port used for new access keys, must be between 1 and 65535. - """ - name: str - serverId: str - metricsEnabled: bool - createdTimestampMs: int = Field(ge=0, description="Timestamp must be non-negative") - portForNewAccessKeys: int = Field(ge=1, le=65535, description="Port must be between 1 and 65535") + bytes: int = Field(gt=0) + @field_validator("bytes") + def validate_bytes(cls, v: int) -> int: + if v < 0: + raise ValueError("bytes must be positive") + return v -class DataLimit(BaseModel): - """ - Model for data limit information. - Attributes: - bytes (int): The data limit in bytes, must be non-negative. - """ - bytes: int = Field(ge=0, description="Data limit in bytes must be non-negative") +class AccessKey(BaseModel): + """Access key details.""" + id: int + name: Optional[str] = None + password: str + port: int = Field(gt=0, lt=65536) + method: str + access_url: str = Field(alias="accessUrl") + data_limit: Optional[DataLimit] = Field(None, alias="dataLimit") -class AccessKey(BaseModel): + +class AccessKeyList(BaseModel): + """List of access keys.""" + + access_keys: list[AccessKey] = Field(alias="accessKeys") + + +class ServerMetrics(BaseModel): """ - Model for access key information. - - Attributes: - id (str): The unique identifier for the access key. - name (str): The name of the access key. - password (str): The password for the access key, must not be empty. - port (int): The port used by the access key, must be between 1 and 65535. - method (str): The encryption method used by the access key. - accessUrl (str): The URL used to access the server, must not be empty. + Server metrics data for data transferred per access key + Per OpenAPI: /metrics/transfer endpoint """ - id: str - name: str - password: str = Field(..., min_length=1, description="Password must not be empty") - port: int = Field(ge=1, le=65535, description="Port must be between 1 and 65535") - method: str - accessUrl: str = Field(..., min_length=1, description="Access URL must not be empty") + bytes_transferred_by_user_id: dict[str, int] = Field( + alias="bytesTransferredByUserId" + ) -class ServerPort(BaseModel): - """ - Model for server port information. - Attributes: - port (int): The port used by the server, must be between 1 and 65535. - """ - port: int = Field(ge=1, le=65535, description="Port must be between 1 and 65535") +class TunnelData(BaseModel): + seconds: int -class AccessKeyCreateRequest(BaseModel): - """ - Model for creating access key information. +class TransferData(BaseModel): + bytes: int - Attributes: - name (Optional[str]): The name of the access key (optional). - password (Optional[str]): The password for the access key (optional). - port (Optional[int]): The port used by the access key, must be between 0 and 65535 (optional). - """ - name: Optional[str] - password: Optional[str] - port: Optional[int] = Field(ge=0, le=65535, description="Port must be between 0 and 65535") +class ServerMetric(BaseModel): + location: str + asn: Optional[int] = None + as_org: Optional[str] = Field(None, alias="asOrg") + tunnel_time: TunnelData = Field(alias="tunnelTime") + data_transferred: TransferData = Field(alias="dataTransferred") -class AccessKeyList(BaseModel): - """ - Model for access key list information. - Attributes: - accessKeys (List[AccessKey]): A list of access keys. - """ - accessKeys: List[AccessKey] +class AccessKeyMetric(BaseModel): + access_key_id: int = Field(alias="accessKeyId") + tunnel_time: TunnelData = Field(alias="tunnelTime") + data_transferred: TransferData = Field(alias="dataTransferred") -class MetricsEnabled(BaseModel): +class ExperimentalMetrics(BaseModel): """ - Model for metrics enabled information. + Experimental metrics data structure + Per OpenAPI: /experimental/server/metrics endpoint + """ + + server: list[ServerMetric] + access_keys: list[AccessKeyMetric] = Field(alias="accessKeys") + - Attributes: - enabled (bool): Indicates if metrics collection is enabled. +class Server(BaseModel): + """ + Server information. + Per OpenAPI: /server endpoint schema """ - enabled: bool + + name: str + server_id: str = Field(alias="serverId") + metrics_enabled: bool = Field(alias="metricsEnabled") + created_timestamp_ms: int = Field(alias="createdTimestampMs") + version: str + port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536) + hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys") + access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit") -class Metrics(BaseModel): +class AccessKeyCreateRequest(BaseModel): """ - Model for metrics information. + Request parameters for creating an access key. + Per OpenAPI: /access-keys POST request body + """ + + name: Optional[str] = None + method: Optional[str] = None + password: Optional[str] = None + port: Optional[int] = Field(None, gt=0, lt=65536) + limit: Optional[DataLimit] = None - Attributes: - bytesTransferredByUserId (Dict[str, int]): A dictionary mapping user IDs to the number of bytes transferred. - User IDs must be non-empty strings, and byte values must be non-negative. - Methods: - validate_bytes_transferred: Validates that all byte values in the dictionary are non-negative. +class MetricsStatusResponse(BaseModel): + """Response for /metrics/enabled endpoint""" + + metrics_enabled: bool = Field(alias="metricsEnabled") + + +class ErrorResponse(BaseModel): """ - bytesTransferredByUserId: Dict[constr(min_length=1), int] = Field( - description="User IDs must be non-empty strings and byte values must be non-negative") - - @field_validator("bytesTransferredByUserId") - def validate_bytes_transferred(cls, value: Dict[str, int]) -> Dict[str, int]: - """ - Validate that all byte values in the dictionary are non-negative. - - Args: - value (Dict[str, int]): The dictionary to validate. - - Returns: - Dict[str, int]: The validated dictionary. - - Raises: - ValueError: If any byte value is negative. - """ - for user_id, bytes_transferred in value.items(): - if bytes_transferred < 0: - raise ValueError(f"Transferred bytes for user {user_id} must be non-negative") - return value + Error response structure + Per OpenAPI: 404 and 400 responses + """ + + code: str + message: str diff --git a/pyproject.toml b/pyproject.toml index 62e8a24..74bb1b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,27 +1,40 @@ [tool.poetry] name = "pyoutlineapi" -version = "0.1.3" -description = "A Python package to interact with the Outline VPN Server API" +version = "0.2.0" +description = "A modern, async-first Python client for the Outline VPN Server API with comprehensive data validation through Pydantic models." authors = ["Denis Rozhnovskiy "] readme = "README.md" license = "MIT" packages = [{ include = "pyoutlineapi" }] -keywords = ["outline", "vpn", "api", "manager", "wrapper"] +keywords = ["outline", "vpn", "api", "manager", "wrapper", "asyncio"] classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Programming Language :: Python :: 3 :: Only", + "Typing :: Typed", + "Development Status :: 5 - Production/Stable", + "Framework :: AsyncIO", + "Framework :: aiohttp", + "Framework :: Pydantic", ] [tool.poetry.dependencies] -python = ">=3.10" +python = ">=3.10,<4.0" pydantic = "^2.9.2" -requests = "^2.32.3" -requests-toolbelt = "^1.0.0" +aiohttp = "^3.11.11" [tool.poetry.group.dev.dependencies] pytest-cov = "^5.0.0" +black = "^24.10.0" +mypy = "^1.0.0" +flake8 = "^6.0.0" [tool.pytest.ini_options] addopts = "--cov=pyoutlineapi --cov-report=term" diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index 27c94f9..0000000 --- a/tests/test_client.py +++ /dev/null @@ -1,133 +0,0 @@ -import unittest -from unittest.mock import patch, Mock - -import requests - -from pyoutlineapi.client import PyOutlineWrapper -from pyoutlineapi.exceptions import APIError -from pyoutlineapi.models import Server, DataLimit - - -class TestPyOutlineWrapper(unittest.TestCase): - - def setUp(self): - self.api_url = "https://example.com" - self.cert_sha256 = "dummy-sha256" - self.wrapper = PyOutlineWrapper(api_url=self.api_url, cert_sha256=self.cert_sha256) - - @patch("pyoutlineapi.client.requests.Session.request") - def test_request_success(self, mock_request): - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {} - mock_request.return_value = mock_response - - response = self.wrapper._request("GET", "test-endpoint") - self.assertEqual(response.status_code, 200) - - @patch("pyoutlineapi.client.requests.Session.request") - def test_request_failure(self, mock_request): - mock_request.side_effect = requests.RequestException("Connection error") - - with self.assertRaises(APIError): - self.wrapper._request("GET", "test-endpoint") - - @patch("pyoutlineapi.client.requests.Response.json") - def test_parse_response_success(self, mock_json): - mock_json.return_value = { - "name": "Test Server", - "serverId": "12345", - "metricsEnabled": True, - "createdTimestampMs": 1609459200000, - "portForNewAccessKeys": 8080 - } - - response = Mock() - response.json = mock_json - - result = self.wrapper._parse_response(response, Server) - self.assertIsInstance(result, str) # JSON format is True by default - - @patch.object(PyOutlineWrapper, '_request') - def test_get_server_info(self, mock_request): - mock_request.return_value.json.return_value = { - "name": "Test Server", - "serverId": "12345", - "metricsEnabled": True, - "createdTimestampMs": 1609459200000, - "portForNewAccessKeys": 8080 - } - - result = self.wrapper.get_server_info() - self.assertIsInstance(result, str) - - @patch.object(PyOutlineWrapper, '_request') - def test_create_access_key(self, mock_request): - mock_request.return_value.json.return_value = { - "id": "test_id", - "name": "Test Access Key", - "password": "secret", - "port": 8080, - "method": "aes-256-gcm", - "accessUrl": "ss://..." - } - - result = self.wrapper.create_access_key(name="Test Access Key", password="secret", port=8080) - self.assertIsInstance(result, str) - - @patch.object(PyOutlineWrapper, '_request') - def test_get_access_keys(self, mock_request): - mock_request.return_value.json.return_value = { - "accessKeys": [{ - "id": "test_id", - "name": "Test Access Key", - "password": "secret", - "port": 8080, - "method": "aes-256-gcm", - "accessUrl": "ss://..." - }] - } - - result = self.wrapper.get_access_keys() - self.assertIsInstance(result, str) - - @patch.object(PyOutlineWrapper, '_request') - def test_delete_access_key(self, mock_request): - mock_request.return_value.status_code = 204 - - result = self.wrapper.delete_access_key("test_id") - self.assertTrue(result) - - @patch.object(PyOutlineWrapper, '_request') - def test_update_server_port(self, mock_request): - mock_request.return_value.status_code = 204 - - result = self.wrapper.update_server_port(8080) - self.assertTrue(result) - - @patch.object(PyOutlineWrapper, '_request') - def test_set_access_key_data_limit(self, mock_request): - mock_request.return_value.status_code = 204 - - data_limit = DataLimit(bytes=1000000) - result = self.wrapper.set_access_key_data_limit("test_id", data_limit) - self.assertTrue(result) - - @patch.object(PyOutlineWrapper, '_request') - def test_remove_access_key_data_limit(self, mock_request): - mock_request.return_value.status_code = 204 - - result = self.wrapper.remove_access_key_data_limit("test_id") - self.assertTrue(result) - - @patch.object(PyOutlineWrapper, '_request') - def test_get_metrics(self, mock_request): - mock_request.return_value.json.return_value = { - "bytesTransferredByUserId": { - "user1": 1000, - "user2": 2000 - } - } - - result = self.wrapper.get_metrics() - self.assertIsInstance(result, str) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py deleted file mode 100644 index aed32ff..0000000 --- a/tests/test_exceptions.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. - -""" - -import unittest -from pyoutlineapi.exceptions import APIError, HTTPError, RequestError, ValidationError - -class TestAPIError(unittest.TestCase): - def test_api_error_initialization(self): - """Test initialization of APIError.""" - error = APIError("Test API Error") - self.assertEqual(str(error), "Test API Error") - self.assertEqual(error.message, "Test API Error") - -class TestHTTPError(unittest.TestCase): - def test_http_error_initialization(self): - """Test initialization of HTTPError.""" - error = HTTPError(404, "Not Found") - self.assertEqual(str(error), "HTTP error occurred: 404 - Not Found") - self.assertEqual(error.status_code, 404) - self.assertEqual(error.message, "Not Found") - -class TestRequestError(unittest.TestCase): - def test_request_error_initialization(self): - """Test initialization of RequestError.""" - error = RequestError("Connection failed") - self.assertEqual(str(error), "An error occurred while requesting data: Connection failed") - -class TestValidationError(unittest.TestCase): - def test_validation_error_initialization(self): - """Test initialization of ValidationError.""" - error = ValidationError("Invalid data format") - self.assertEqual(str(error), "Validation error occurred: Invalid data format") - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/test_logger.py b/tests/test_logger.py deleted file mode 100644 index a3f6f69..0000000 --- a/tests/test_logger.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. - -""" -import io -import logging -import unittest -from logging import StreamHandler - -from pyoutlineapi import logger - - -class TestLoggerSetup(unittest.TestCase): - def setUp(self): - """Setup for test cases.""" - self.logger_name = 'test_logger' - self.logger = logger.setup_logger(self.logger_name) - self.log_stream = io.StringIO() - # Redirect log output to the StringIO object - handler = StreamHandler(self.log_stream) - handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) - self.logger.addHandler(handler) - self.logger.setLevel(logging.DEBUG) - - def tearDown(self): - """Teardown for test cases.""" - self.logger.handlers.clear() - - def test_logger_name(self): - """Test if the logger has the correct name.""" - self.assertEqual(self.logger.name, self.logger_name) - - def test_logger_level(self): - """Test if the logger's level is set to DEBUG.""" - self.assertEqual(self.logger.level, logging.DEBUG) - - def test_logging_format(self): - """Test if the logging format is correct.""" - self.logger.info('Test message') - log_contents = self.log_stream.getvalue() - self.assertIn('Test message', log_contents) - self.assertIn('INFO', log_contents) - self.assertIn(self.logger_name, log_contents) - self.assertIn(' - ', log_contents) - self.assertIn('-', log_contents) # Ensure the format contains '-' - - def test_logger_output(self): - """Test if the log message is correctly output.""" - self.logger.info('Test log output') - log_contents = self.log_stream.getvalue() - self.assertIn('Test log output', log_contents) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 26e99d4..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Copyright (c) 2024 Denis Rozhnovskiy - -This file is part of the PyOutlineAPI project. - -PyOutlineAPI is a Python package for interacting with the Outline VPN Server. - -Licensed under the MIT License. See the LICENSE file for more details. -""" - -import unittest - -from pydantic import ValidationError - -from pyoutlineapi.models import Server, ServerPort, DataLimit, AccessKey, Metrics - - -class TestPyOutlineModels(unittest.TestCase): - - def test_server_model_invalid_timestamp(self): - """Test that Server model raises ValidationError for invalid timestamp.""" - with self.assertRaises(ValidationError): - Server( - name="Test Server", - serverId="server-id", - metricsEnabled=True, - createdTimestampMs=-1609459200000, # Invalid negative timestamp - portForNewAccessKeys=12345 - ) - - def test_server_port_invalid_range(self): - """Test that ServerPort model raises ValidationError for port out of range.""" - with self.assertRaises(ValidationError): - ServerPort(port=70000) # Port number out of valid range - - def test_data_limit_negative_bytes(self): - """Test that DataLimit model raises ValidationError for negative bytes.""" - with self.assertRaises(ValidationError): - DataLimit(bytes=-1) # Negative value not allowed - - def test_access_key_invalid_port(self): - """Test that AccessKey model raises ValidationError for invalid port.""" - with self.assertRaises(ValidationError): - AccessKey( - id="access-key-id", - name="test-key", - password="test-password", - port=70000, # Invalid port number - method="aes-256-cfb", - accessUrl="ss://example" - ) - - def test_metrics_invalid_data(self): - """Test that Metrics model raises ValidationError for invalid dictionary data.""" - with self.assertRaises(ValidationError): - Metrics(bytesTransferredByUserId={"user1": -100}) # Negative bytes transferred - - def test_access_key_empty_password(self): - """Test that AccessKey model raises ValidationError for empty password.""" - with self.assertRaises(ValidationError): - AccessKey( - id="access-key-id", - name="test-key", - password="", # Empty password - port=12345, - method="aes-256-cfb", - accessUrl="ss://example" - ) - - def test_access_key_invalid_url(self): - """Test that AccessKey model raises ValidationError for invalid accessUrl.""" - with self.assertRaises(ValidationError): - AccessKey( - id="access-key-id", - name="test-key", - password="test-password", - port=12345, - method="aes-256-cfb", - accessUrl="" # Invalid access URL - ) - - -if __name__ == "__main__": - unittest.main() From a1276ccd6aa376ff400effe822ad698f54aae992 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Thu, 9 Jan 2025 22:08:34 +0500 Subject: [PATCH 06/24] fix: change email --- README.md | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 66c9335..a388f5f 100644 --- a/README.md +++ b/README.md @@ -133,20 +133,14 @@ from pyoutlineapi.models import MetricsPeriod async def get_metrics(): - async with AsyncOutlineClient(...) as client: - # Enable metrics collection - await client.set_metrics_status(True) - - # Get transfer metrics - metrics = await client.get_transfer_metrics(MetricsPeriod.MONTHLY) - for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items(): - print(f"User {user_id}: {bytes_transferred / 1024 ** 3:.2f} GB") - - # Get detailed metrics - detailed = await client.get_experimental_metrics() - for server in detailed.server: - print(f"Location: {server.location}") - print(f"Data: {server.data_transferred.bytes / 1024 ** 2:.2f} MB") + async with AsyncOutlineClient(...) as client: + # Enable metrics collection + await client.set_metrics_status(True) + + # Get transfer metrics + metrics = await client.get_transfer_metrics(MetricsPeriod.MONTHLY) + for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items(): + print(f"User {user_id}: {bytes_transferred / 1024 ** 3:.2f} GB") ``` ## Error Handling @@ -174,7 +168,7 @@ requests, report issues, and contribute to the project. ## Security -If you discover any security-related issues, please email security@example.com instead of using the issue tracker. +If you discover any security-related issues, please email `pytelemonbot@mail.ru` instead of using the issue tracker. ## License From c83a0d3215c2956e22c03f0d11f11c4e8500b2ad Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 01:35:25 +0500 Subject: [PATCH 07/24] docs: update docs - Added CHANGELOG.md - Update CONTRIBUTING.md --- CHANGELOG.md | 55 +++++++++++++ CONTRIBUTING.md | 211 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 219 insertions(+), 47 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7d25659 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2024-01-10 + +### Added + +- New asynchronous client `AsyncOutlineClient` using `aiohttp` +- Comprehensive type hints and overloads for better IDE support +- New methods for server management: + - `rename_server()` - Change server name + - `set_hostname()` - Configure server hostname + - `get_metrics_status()` - Check metrics collection status + - `set_metrics_status()` - Enable/disable metrics collection +- Support for different metrics periods (DAILY, WEEKLY, MONTHLY) +- Extended options for access key creation (method, encryption settings) +- Improved error handling with detailed error messages +- Context manager support with async `__aenter__` and `__aexit__` + +### Changed + +- Complete rewrite of the client to support asynchronous operations +- Enhanced error hierarchy with `OutlineError` base class +- Improved request handling with automatic session management +- More flexible SSL/TLS certificate verification +- Better JSON response parsing and validation +- Updated type annotations to use modern Python typing features + +### Removed + +- Synchronous client implementation (migrated to async) +- Direct requests-based HTTP handling + +## [0.1.2] - 2024-01-09 + +### Added + +- Initial release with synchronous client +- Basic Outline VPN server management features: + - Server information retrieval + - Access key management (create, list, delete) + - Data limit management + - Server port configuration + - Basic metrics retrieval +- Pydantic models for data validation +- Support for custom certificate verification +- Optional JSON response format + +[0.2.0]: https://github.com/username/repo/compare/v0.1.2...v0.2.0 + +[0.1.2]: https://github.com/username/repo/releases/tag/v0.1.2 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 547cf05..26f9945 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to PyOutlineAPI Thank you for considering contributing to PyOutlineAPI! Whether you have suggestions, bug reports, or code improvements, -your input is valuable. Here are some guidelines to help you contribute effectively: +your input is valuable. ## How to Contribute @@ -9,87 +9,204 @@ your input is valuable. Here are some guidelines to help you contribute effectiv If you encounter any issues or bugs, please follow these steps: -1. **Search for Existing Issues**: Check the [Issues](https://github.com/orenlab/pyoutlineapi/issues) section to see - if your issue has already been reported. +1. **Search for Existing Issues**: Check the [Issues](https://github.com/orenlab/pyoutlineapi/issues) section to see if + your issue has already been reported. 2. **Create a New Issue**: If you don't find an existing - issue, [open a new issue](https://github.com/orenlab/pyoutlineapi/issues/new) with a clear description of the - problem. Include: + issue, [open a new issue](https://github.com/orenlab/pyoutlineapi/issues/new) with: - A descriptive title - Steps to reproduce the issue - Expected and actual results - - Any relevant code snippets or logs + - Python version (3.10+) and PyOutlineAPI version + - Any relevant code snippets or error messages + - Environment details (OS, Outline server version) ### Suggesting Enhancements -If you have an idea for a new feature or improvement: +For new feature or improvement suggestions: -1. **Check Existing Feature Requests**: Look through the [Issues](https://github.com/orenlab/pyoutlineapi/issues) to - see if a similar feature has already been suggested. +1. **Check Existing Feature Requests**: Review the [Issues](https://github.com/orenlab/pyoutlineapi/issues) to see if + similar features have been suggested. 2. **Open a New Feature Request**: [Submit a new feature request](https://github.com/orenlab/pyoutlineapi/issues/new) with: - A descriptive title - - A detailed description of the proposed feature + - Detailed description of the proposed feature - Use cases and benefits - - Any related documentation or examples + - Example code or API design if applicable ### Contributing Code To contribute code: -1. **Fork the Repository**: Create a fork of the repository on GitHub. -2. **Clone Your Fork**: Clone your fork locally. +1. **Fork and Clone**: ```bash git clone https://github.com/orenlab/pyoutlineapi.git + cd pyoutlineapi ``` -3. **Create a New Branch**: Create a new branch for your changes. + +2. **Set Up Development Environment**: ```bash - git checkout -b my-feature-branch + # Install Poetry if you haven't already + curl -sSL https://install.python-poetry.org | python3 - + + # Install dependencies + poetry install + + # Activate virtual environment + poetry shell ``` -4. **Make Your Changes**: Implement your changes, ensuring to follow existing code styles and conventions. -5. **Write Tests**: Add or update tests to cover your changes. -6. **Run Tests**: Ensure all tests pass before submitting a pull request. -7. **Commit and Push**: Commit your changes and push them to your fork. + +3. **Create a Feature Branch**: ```bash - git add . - git commit -m "Add new feature or fix bug" - git push origin my-feature-branch + git checkout -b feature/your-feature-name + # or + git checkout -b fix/issue-description ``` -8. **Submit a Pull Request**: Open a pull request from your branch to the `main` branch of the original repository. - Provide a clear description of your changes and any relevant details. -## Code of Conduct +4. **Make Your Changes**: + - Follow the existing code structure + - Use type hints consistently (Python 3.10+ typing features) + - Add docstrings with examples (see existing code) + - Update tests if needed -Please adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) in all interactions. Respectful and constructive -communication is essential for a positive and productive community. +5. **Test Your Changes**: + ```bash + # Run tests with coverage + poetry run pytest -## Style Guide + # Type checking + poetry run mypy pyoutlineapi -Follow these guidelines to ensure consistency in the codebase: + # Code formatting + poetry run black pyoutlineapi tests -- **Code Style**: Adhere to [PEP 8](https://pep8.org/) for Python code style. -- **Documentation**: Update documentation as needed to reflect code changes. Use clear, concise language and proper - formatting. -- **Commit Messages**: Write clear and descriptive commit messages. Use the following format: - ``` - [type]: [short summary] + # Linting + poetry run flake8 pyoutlineapi tests + ``` - [longer description, if necessary] +6. **Submit a Pull Request**: + - Write a clear PR description + - Link related issues + - Include any necessary documentation updates + +## Code Style Guidelines + +We follow strict coding standards to maintain consistency: + +### Python Style + +- Follow [PEP 8](https://pep8.org/) conventions +- Use modern type hints (Python 3.10+) +- Maximum line length: 88 characters (Black default) +- Use descriptive variable names + +### Documentation + +- Use Google-style docstrings with type information +- For non-private methods, include examples in docstrings (see existing code) +- Example: + ```python + async def create_access_key( + self, + *, + name: Optional[str] = None, + port: Optional[int] = None, + ) -> Union[JsonDict, AccessKey]: + """ + Create a new access key. + + Args: + name: Optional key name + port: Optional port number (1-65535) + + Returns: + New access key details + + Examples: + >>> async with AsyncOutlineClient(...) as client: + ... key = await client.create_access_key(name="User 1") + ... print(f"Created key: {key.access_url}") + """ ``` - Types of commits might include: - - `feat`: A new feature - - `fix`: A bug fix - - `docs`: Documentation changes - - `style`: Code style improvements (non-functional changes) - - `refactor`: Code refactoring (no functional changes) - - `test`: Adding or updating tests - - `chore`: Other changes (e.g., build process, CI configuration) +### Testing + +- Write unit tests for new features +- Use pytest fixtures and parametrize when appropriate +- Mock external dependencies +- Maintain high test coverage (enforced by pytest-cov) + +### Commit Messages + +Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +``` +(): + +[optional body] + +[optional footer] +``` + +Types: + +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `style`: Code style/formatting changes +- `refactor`: Code refactoring +- `test`: Adding/updating tests +- `chore`: Maintenance tasks + +Example: + +``` +feat(client): add support for custom encryption methods + +- Added method parameter to create_access_key +- Updated documentation with examples +- Added unit tests for new functionality + +Closes #123 +``` + +## Development Setup + +1. **Required Dependencies**: + - Python 3.10 or higher + - Poetry for package management + - Outline server (for integration testing) + +2. **Development Tools**: + All development dependencies are managed by Poetry and include: + - pytest-cov for test coverage + - black for code formatting + - mypy for type checking + - flake8 for linting + +3. **Environment Variables for Testing**: + ```bash + OUTLINE_API_URL=https://your-server:port/secret + OUTLINE_CERT_SHA256=your-cert-fingerprint + ``` + +## Project Configuration + +Key project settings are managed in `pyproject.toml`, including: + +- Python version requirement (3.10+) +- Dependencies: + - pydantic (^2.9.2) + - aiohttp (^3.11.11) +- Development dependencies for testing and code quality +- Pytest configuration with coverage reporting ## Contact -For any questions or additional information, feel free to reach out: +- **Issues**: [GitHub Issues](https://github.com/orenlab/pyoutlineapi/issues) +- **Email**: `pytelemonbot@mail.ru` + +## License -- **Email**: pytelemonbot@mail.ru -- **GitHub Issues**: [Link](https://github.com/orenlab/pyoutlineapi/issues) +By contributing, you agree that your contributions will be licensed under the MIT License. Thank you for contributing to PyOutlineAPI! \ No newline at end of file From d01c92c6383cd47f59ec747909c5ace4d6df7d09 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 01:35:50 +0500 Subject: [PATCH 08/24] chore: update pyproject.toml --- poetry.lock | 101 +++++++++++++++++++++++-------------------------- pyproject.toml | 30 ++++++++++++++- 2 files changed, 75 insertions(+), 56 deletions(-) diff --git a/poetry.lock b/poetry.lock index eeeab85..bd31a43 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,23 +340,6 @@ files = [ [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "flake8" -version = "6.1.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -groups = ["dev"] -files = [ - {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, - {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" -pyflakes = ">=3.1.0,<3.2.0" - [[package]] name = "frozenlist" version = "1.5.0" @@ -486,18 +469,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "multidict" version = "6.1.0" @@ -824,18 +795,6 @@ files = [ {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, ] -[[package]] -name = "pycodestyle" -version = "2.11.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, -] - [[package]] name = "pydantic" version = "2.10.5" @@ -970,18 +929,6 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pyflakes" -version = "3.1.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, - {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, -] - [[package]] name = "pytest" version = "8.3.4" @@ -1005,6 +952,25 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.25.2" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, + {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pytest-cov" version = "5.0.0" @@ -1024,6 +990,33 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "ruff" +version = "0.3.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, + {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, + {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, + {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, + {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, +] + [[package]] name = "tomli" version = "2.2.1" @@ -1179,4 +1172,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "1cda916d7c46267e462caef25d7dab0bc7b4044c2a0de2efc06f2f50f9ee2470" +content-hash = "f563f761b8c84386e5834219f9b2c9f0e1a87023436c13ce9337e6fa06962997" diff --git a/pyproject.toml b/pyproject.toml index 74bb1b6..5f92655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ readme = "README.md" license = "MIT" packages = [{ include = "pyoutlineapi" }] keywords = ["outline", "vpn", "api", "manager", "wrapper", "asyncio"] +documentation = "https://github.com/orenlab/pyoutlineapi/blob/main/README.md" classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", @@ -23,6 +24,8 @@ classifiers = [ "Framework :: AsyncIO", "Framework :: aiohttp", "Framework :: Pydantic", + "Topic :: Security", + "Topic :: Internet :: Proxy Servers", ] [tool.poetry.dependencies] @@ -31,17 +34,40 @@ pydantic = "^2.9.2" aiohttp = "^3.11.11" [tool.poetry.group.dev.dependencies] +pytest = "^8.3.4" +pytest-asyncio = "^0.25.2" pytest-cov = "^5.0.0" black = "^24.10.0" mypy = "^1.0.0" -flake8 = "^6.0.0" +ruff = "^0.3.0" [tool.pytest.ini_options] -addopts = "--cov=pyoutlineapi --cov-report=term" +addopts = "--cov=pyoutlineapi --cov-report=term-missing --cov-report=xml --cov-report=html" +testpaths = ["tests"] +asyncio_mode = "auto" + +[tool.black] +line-length = 88 +target-version = ["py310", "py311", "py312"] +include = '\.pyi?$' + +[tool.mypy] +python_version = "3.10" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + +[tool.ruff] +line-length = 88 +target-version = "py310" +lint.select = ["E", "F", "B", "I"] [tool.poetry.urls] homepage = "https://github.com/orenlab/pyoutlineapi" repository = "https://github.com/orenlab/pyoutlineapi" +documentation = "https://github.com/orenlab/pyoutlineapi/blob/main/README.md" +changelog = "https://github.com/orenlab/pyoutlineapi/blob/main/CHANGELOG.md" [build-system] requires = ["poetry-core>=1.3.0"] From c837eac8d3ce0d256a84fbb253f69bee7cfa82ca Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 01:38:46 +0500 Subject: [PATCH 09/24] chore: reformat code --- pyoutlineapi/client.py | 83 ++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index 14b5f17..726615d 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -53,12 +53,12 @@ class AsyncOutlineClient: """ def __init__( - self, - api_url: str, - cert_sha256: str, - *, - json_format: bool = True, - timeout: float = 30.0, + self, + api_url: str, + cert_sha256: str, + *, + json_format: bool = True, + timeout: float = 30.0, ) -> None: self._api_url = api_url.rstrip("/") self._cert_sha256 = cert_sha256 @@ -90,30 +90,27 @@ def _ensure_context(self): @overload async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: Literal[True], - ) -> JsonDict: - ... + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[True], + ) -> JsonDict: ... @overload async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: Literal[False], - ) -> BaseModel: - ... + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[False], + ) -> BaseModel: ... @overload async def _parse_response( - self, response: ClientResponse, model: type[BaseModel], json_format: bool - ) -> Union[JsonDict, BaseModel]: - ... + self, response: ClientResponse, model: type[BaseModel], json_format: bool + ) -> Union[JsonDict, BaseModel]: ... async def _parse_response( - self, response: ClientResponse, model: type[BaseModel], json_format: bool = True + self, response: ClientResponse, model: type[BaseModel], json_format: bool = True ) -> Union[JsonDict, BaseModel]: """ Parse and validate API response data. @@ -152,12 +149,12 @@ async def _handle_error_response(response: ClientResponse) -> None: raise APIError(f"HTTP {response.status}: {response.reason}") async def _request( - self, - method: str, - endpoint: str, - *, - json: Any = None, - params: Optional[dict[str, Any]] = None, + self, + method: str, + endpoint: str, + *, + json: Any = None, + params: Optional[dict[str, Any]] = None, ) -> Any: """Make an API request.""" self._ensure_context() @@ -166,13 +163,13 @@ async def _request( ssl_context = self._get_ssl_context() async with self._session.request( - method, - url, - json=json, - params=params, - ssl=ssl_context, - raise_for_status=False, - timeout=self._timeout, + method, + url, + json=json, + params=params, + ssl=ssl_context, + raise_for_status=False, + timeout=self._timeout, ) as response: if response.status >= 400: await self._handle_error_response(response) @@ -359,7 +356,7 @@ async def set_metrics_status(self, enabled: bool) -> bool: ) async def get_transfer_metrics( - self, period: MetricsPeriod = MetricsPeriod.MONTHLY + self, period: MetricsPeriod = MetricsPeriod.MONTHLY ) -> Union[JsonDict, ServerMetrics]: """ Get transfer metrics for specified period. @@ -391,13 +388,13 @@ async def get_transfer_metrics( ) async def create_access_key( - self, - *, - name: Optional[str] = None, - password: Optional[str] = None, - port: Optional[int] = None, - method: Optional[str] = None, - limit: Optional[DataLimit] = None, + self, + *, + name: Optional[str] = None, + password: Optional[str] = None, + port: Optional[int] = None, + method: Optional[str] = None, + limit: Optional[DataLimit] = None, ) -> Union[JsonDict, AccessKey]: """ Create a new access key. From 0a6a0f06636876d4026878e59f8c0dc48b80a09d Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 01:52:51 +0500 Subject: [PATCH 10/24] docs: added pdoc docs --- .github/workflows/docs.yml | 49 + docs/index.html | 7 + docs/pyoutlineapi.html | 2833 ++++++++++++++++++++++++++++++++++++ docs/search.js | 46 + 4 files changed, 2935 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/index.html create mode 100644 docs/pyoutlineapi.html create mode 100644 docs/search.js diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..b59f858 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,49 @@ +name: website + +# build the documentation whenever there are new commits on main +on: + push: + branches: + - development + # Alternative: only build for tags. + # tags: + # - '*' + +# security: restrict permissions for CI jobs. +permissions: + contents: read + +jobs: + # Build the documentation and upload the static HTML files as an artifact. + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + + # ADJUST THIS: install all dependencies (including pdoc) + - run: pip install -e . + # ADJUST THIS: build your documentation into docs/. + # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. + - run: python docs/make.py + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/ + + # Deploy the artifact to GitHub pages. + # This is a separate job so that only actions/deploy-pages has the necessary permissions. + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..f272e85 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/pyoutlineapi.html b/docs/pyoutlineapi.html new file mode 100644 index 0000000..68f939b --- /dev/null +++ b/docs/pyoutlineapi.html @@ -0,0 +1,2833 @@ + + + + + + + pyoutlineapi API documentation + + + + + + + + + +
+
+

+pyoutlineapi

+ + + + + + +
 1from .client import AsyncOutlineClient, OutlineError, APIError
+ 2from .models import (
+ 3    AccessKey,
+ 4    AccessKeyCreateRequest,
+ 5    AccessKeyList,
+ 6    DataLimit,
+ 7    ErrorResponse,
+ 8    ExperimentalMetrics,
+ 9    MetricsPeriod,
+10    MetricsStatusResponse,
+11    Server,
+12    ServerMetrics,
+13)
+14
+15__version__ = "0.2.0"
+16
+17__all__ = [
+18    "AsyncOutlineClient",
+19    "OutlineError",
+20    "APIError",
+21    "AccessKey",
+22    "AccessKeyCreateRequest",
+23    "AccessKeyList",
+24    "DataLimit",
+25    "ErrorResponse",
+26    "ExperimentalMetrics",
+27    "MetricsPeriod",
+28    "MetricsStatusResponse",
+29    "Server",
+30    "ServerMetrics",
+31]
+
+ + +
+
+ +
+ + class + AsyncOutlineClient: + + + +
+ +
 37class AsyncOutlineClient:
+ 38    """
+ 39    Asynchronous client for the Outline VPN Server API.
+ 40
+ 41    Args:
+ 42        api_url: Base URL for the Outline server API
+ 43        cert_sha256: SHA-256 fingerprint of the server's TLS certificate
+ 44        json_format: Return raw JSON instead of Pydantic models
+ 45        timeout: Request timeout in seconds
+ 46
+ 47    Examples:
+ 48        >>> async def doo_something():
+ 49        ...     async with AsyncOutlineClient(
+ 50        ...         "https://example.com:1234/secret",
+ 51        ...         "ab12cd34..."
+ 52        ...     ) as client:
+ 53        ...         server_info = await client.get_server_info()
+ 54    """
+ 55
+ 56    def __init__(
+ 57        self,
+ 58        api_url: str,
+ 59        cert_sha256: str,
+ 60        *,
+ 61        json_format: bool = True,
+ 62        timeout: float = 30.0,
+ 63    ) -> None:
+ 64        self._api_url = api_url.rstrip("/")
+ 65        self._cert_sha256 = cert_sha256
+ 66        self._json_format = json_format
+ 67        self._timeout = aiohttp.ClientTimeout(total=timeout)
+ 68        self._ssl_context = None
+ 69        self._session: Optional[aiohttp.ClientSession] = None
+ 70        self._in_context = False
+ 71
+ 72    async def __aenter__(self) -> AsyncOutlineClient:
+ 73        """Set up client session for context manager."""
+ 74        self._session = aiohttp.ClientSession(
+ 75            timeout=self._timeout, raise_for_status=True
+ 76        )
+ 77        self._in_context = True
+ 78        return self
+ 79
+ 80    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+ 81        """Clean up client session."""
+ 82        if self._session:
+ 83            await self._session.close()
+ 84            self._session = None
+ 85        self._in_context = False
+ 86
+ 87    def _ensure_context(self):
+ 88        """Ensure the session context is valid."""
+ 89        if not self._session or self._session.closed:
+ 90            raise RuntimeError("Client session is not initialized or already closed.")
+ 91
+ 92    @overload
+ 93    async def _parse_response(
+ 94        self,
+ 95        response: ClientResponse,
+ 96        model: type[BaseModel],
+ 97        json_format: Literal[True],
+ 98    ) -> JsonDict: ...
+ 99
+100    @overload
+101    async def _parse_response(
+102        self,
+103        response: ClientResponse,
+104        model: type[BaseModel],
+105        json_format: Literal[False],
+106    ) -> BaseModel: ...
+107
+108    @overload
+109    async def _parse_response(
+110        self, response: ClientResponse, model: type[BaseModel], json_format: bool
+111    ) -> Union[JsonDict, BaseModel]: ...
+112
+113    async def _parse_response(
+114        self, response: ClientResponse, model: type[BaseModel], json_format: bool = True
+115    ) -> Union[JsonDict, BaseModel]:
+116        """
+117        Parse and validate API response data.
+118
+119        Args:
+120            response: API response to parse
+121            model: Pydantic model for validation
+122            json_format: Whether to return raw JSON
+123
+124        Returns:
+125            Validated response data
+126
+127        Raises:
+128            ValueError: If response validation fails
+129        """
+130        self._ensure_context()
+131
+132        try:
+133            data = await response.json()
+134        except aiohttp.ContentTypeError:
+135            raise ValueError("Invalid response format") from None
+136        try:
+137            validated = model.model_validate(data)
+138            return validated.model_dump() if json_format else validated
+139        except Exception as e:
+140            raise ValueError(f"Value error: {e}") from e
+141
+142    @staticmethod
+143    async def _handle_error_response(response: ClientResponse) -> None:
+144        """Handle error responses from the API."""
+145        try:
+146            error_data = await response.json()
+147            error = ErrorResponse.model_validate(error_data)
+148            raise APIError(f"{error.code}: {error.message}")
+149        except ValueError:
+150            raise APIError(f"HTTP {response.status}: {response.reason}")
+151
+152    async def _request(
+153        self,
+154        method: str,
+155        endpoint: str,
+156        *,
+157        json: Any = None,
+158        params: Optional[dict[str, Any]] = None,
+159    ) -> Any:
+160        """Make an API request."""
+161        self._ensure_context()
+162
+163        url = self._build_url(endpoint)
+164        ssl_context = self._get_ssl_context()
+165
+166        async with self._session.request(
+167            method,
+168            url,
+169            json=json,
+170            params=params,
+171            ssl=ssl_context,
+172            raise_for_status=False,
+173            timeout=self._timeout,
+174        ) as response:
+175            if response.status >= 400:
+176                await self._handle_error_response(response)
+177
+178            if response.status == 204:
+179                return True  # No content response
+180
+181            try:
+182                await response.json()
+183                return response
+184            except aiohttp.ContentTypeError:
+185                return await response.text()  # Fallback for non-JSON responses
+186            except Exception as e:
+187                raise APIError(f"Failed to parse response from {url}: {e}") from e
+188
+189    def _build_url(self, endpoint: str) -> str:
+190        """Build and validate the full URL for the API request."""
+191        if not isinstance(endpoint, str):
+192            raise ValueError("Endpoint must be a string")
+193
+194        endpoint = endpoint.lstrip("/")
+195        url = f"{self._api_url}/{endpoint}"
+196
+197        parsed_url = urlparse(url)
+198        if not parsed_url.scheme or not parsed_url.netloc:
+199            raise ValueError(f"Invalid URL: {url}")
+200
+201        return url
+202
+203    def _get_ssl_context(self) -> Optional[Fingerprint]:
+204        """Create an SSL context if a certificate fingerprint is provided."""
+205        if not self._cert_sha256:
+206            return None
+207
+208        try:
+209            fingerprint = binascii.unhexlify(self._cert_sha256)
+210            return Fingerprint(fingerprint)
+211        except binascii.Error as e:
+212            raise ValueError(f"Invalid certificate SHA256: {self._cert_sha256}") from e
+213        except Exception as e:
+214            raise OutlineError("Error while creating SSL context") from e
+215
+216    async def get_server_info(self) -> Union[JsonDict, Server]:
+217        """
+218        Get server information.
+219
+220        Returns:
+221            Server information including name, ID, and configuration.
+222
+223        Examples:
+224            >>> async def doo_something():
+225            ...     async with AsyncOutlineClient(
+226            ...         "https://example.com:1234/secret",
+227            ...         "ab12cd34..."
+228            ...     ) as client:
+229            ...         server = await client.get_server_info()
+230            ...         print(f"Server {server.name} running version {server.version}")
+231        """
+232        response = await self._request("GET", "server")
+233        return await self._parse_response(
+234            response, Server, json_format=self._json_format
+235        )
+236
+237    async def rename_server(self, name: str) -> bool:
+238        """
+239        Rename the server.
+240
+241        Args:
+242            name: New server name
+243
+244        Returns:
+245            True if successful
+246
+247        Examples:
+248            >>> async def doo_something():
+249            ...     async with AsyncOutlineClient(
+250            ...         "https://example.com:1234/secret",
+251            ...         "ab12cd34..."
+252            ...     ) as client:
+253            ...     success = await client.rename_server("My VPN Server")
+254            ...     if success:
+255            ...         print("Server renamed successfully")
+256        """
+257        return await self._request("PUT", "name", json={"name": name})
+258
+259    async def set_hostname(self, hostname: str) -> bool:
+260        """
+261        Set server hostname for access keys.
+262
+263        Args:
+264            hostname: New hostname or IP address
+265
+266        Returns:
+267            True if successful
+268
+269        Raises:
+270            APIError: If hostname is invalid
+271
+272        Examples:
+273            >>> async def doo_something():
+274            ...     async with AsyncOutlineClient(
+275            ...         "https://example.com:1234/secret",
+276            ...         "ab12cd34..."
+277            ...     ) as client:
+278            ...         await client.set_hostname("vpn.example.com")
+279            ...         # Or use IP address
+280            ...         await client.set_hostname("203.0.113.1")
+281        """
+282        return await self._request(
+283            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
+284        )
+285
+286    async def set_default_port(self, port: int) -> bool:
+287        """
+288        Set default port for new access keys.
+289
+290        Args:
+291            port: Port number (1025-65535)
+292
+293        Returns:
+294            True if successful
+295
+296        Raises:
+297            APIError: If port is invalid or in use
+298
+299        Examples:
+300            >>> async def doo_something():
+301            ...     async with AsyncOutlineClient(
+302            ...         "https://example.com:1234/secret",
+303            ...         "ab12cd34..."
+304            ...     ) as client:
+305            ...         await client.set_default_port(8388)
+306
+307        """
+308        return await self._request(
+309            "PUT", "server/port-for-new-access-keys", json={"port": port}
+310        )
+311
+312    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
+313        """
+314        Get whether metrics collection is enabled.
+315
+316        Returns:
+317            Current metrics collection status
+318
+319        Examples:
+320            >>> async def doo_something():
+321            ...     async with AsyncOutlineClient(
+322            ...         "https://example.com:1234/secret",
+323            ...         "ab12cd34..."
+324            ...     ) as client:
+325            ...         if await client.get_metrics_status():
+326            ...             print("Metrics collection is enabled")
+327        """
+328        response = await self._request("GET", "metrics/enabled")
+329        data = await self._parse_response(
+330            response, MetricsStatusResponse, json_format=self._json_format
+331        )
+332        return data
+333
+334    async def set_metrics_status(self, enabled: bool) -> bool:
+335        """
+336        Enable or disable metrics collection.
+337
+338        Args:
+339            enabled: Whether to enable metrics
+340
+341        Returns:
+342            True if successful
+343
+344        Examples:
+345            >>> async def doo_something():
+346            ...     async with AsyncOutlineClient(
+347            ...         "https://example.com:1234/secret",
+348            ...         "ab12cd34..."
+349            ...     ) as client:
+350            ...         # Enable metrics
+351            ...         await client.set_metrics_status(True)
+352            ...         # Check new status
+353            ...         is_enabled = await client.get_metrics_status()
+354        """
+355        return await self._request(
+356            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
+357        )
+358
+359    async def get_transfer_metrics(
+360        self, period: MetricsPeriod = MetricsPeriod.MONTHLY
+361    ) -> Union[JsonDict, ServerMetrics]:
+362        """
+363        Get transfer metrics for specified period.
+364
+365        Args:
+366            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
+367
+368        Returns:
+369            Transfer metrics data for each access key
+370
+371        Examples:
+372            >>> async def doo_something():
+373            ...     async with AsyncOutlineClient(
+374            ...         "https://example.com:1234/secret",
+375            ...         "ab12cd34..."
+376            ...     ) as client:
+377            ...         # Get monthly metrics
+378            ...         metrics = await client.get_transfer_metrics()
+379            ...         # Or get daily metrics
+380            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+381            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+382            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+383        """
+384        response = await self._request(
+385            "GET", "metrics/transfer", params={"period": period.value}
+386        )
+387        return await self._parse_response(
+388            response, ServerMetrics, json_format=self._json_format
+389        )
+390
+391    async def create_access_key(
+392        self,
+393        *,
+394        name: Optional[str] = None,
+395        password: Optional[str] = None,
+396        port: Optional[int] = None,
+397        method: Optional[str] = None,
+398        limit: Optional[DataLimit] = None,
+399    ) -> Union[JsonDict, AccessKey]:
+400        """
+401        Create a new access key.
+402
+403        Args:
+404            name: Optional key name
+405            password: Optional password
+406            port: Optional port number (1-65535)
+407            method: Optional encryption method
+408            limit: Optional data transfer limit
+409
+410        Returns:
+411            New access key details
+412
+413        Examples:
+414            >>> async def doo_something():
+415            ...     async with AsyncOutlineClient(
+416            ...         "https://example.com:1234/secret",
+417            ...         "ab12cd34..."
+418            ...     ) as client:
+419            ...         # Create basic key
+420            ...         key = await client.create_access_key(name="User 1")
+421            ...
+422            ...         # Create key with data limit
+423            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+424            ...         key = await client.create_access_key(
+425            ...             name="Limited User",
+426            ...             port=8388,
+427            ...             limit=_limit
+428            ...         )
+429            ...         print(f"Created key: {key.access_url}")
+430        """
+431        request = AccessKeyCreateRequest(
+432            name=name, password=password, port=port, method=method, limit=limit
+433        )
+434        response = await self._request(
+435            "POST", "access-keys", json=request.model_dump(exclude_none=True)
+436        )
+437        return await self._parse_response(
+438            response, AccessKey, json_format=self._json_format
+439        )
+440
+441    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
+442        """
+443        Get all access keys.
+444
+445        Returns:
+446            List of all access keys
+447
+448        Examples:
+449            >>> async def doo_something():
+450            ...     async with AsyncOutlineClient(
+451            ...         "https://example.com:1234/secret",
+452            ...         "ab12cd34..."
+453            ...     ) as client:
+454            ...         keys = await client.get_access_keys()
+455            ...         for key in keys.access_keys:
+456            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+457            ...             if key.data_limit:
+458            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+459        """
+460        response = await self._request("GET", "access-keys")
+461        return await self._parse_response(
+462            response, AccessKeyList, json_format=self._json_format
+463        )
+464
+465    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
+466        """
+467        Get specific access key.
+468
+469        Args:
+470            key_id: Access key ID
+471
+472        Returns:
+473            Access key details
+474
+475        Raises:
+476            APIError: If key doesn't exist
+477
+478        Examples:
+479            >>> async def doo_something():
+480            ...     async with AsyncOutlineClient(
+481            ...         "https://example.com:1234/secret",
+482            ...         "ab12cd34..."
+483            ...     ) as client:
+484            ...         key = await client.get_access_key(1)
+485            ...         print(f"Port: {key.port}")
+486            ...         print(f"URL: {key.access_url}")
+487        """
+488        response = await self._request("GET", f"access-keys/{key_id}")
+489        return await self._parse_response(
+490            response, AccessKey, json_format=self._json_format
+491        )
+492
+493    async def rename_access_key(self, key_id: int, name: str) -> bool:
+494        """
+495        Rename access key.
+496
+497        Args:
+498            key_id: Access key ID
+499            name: New name
+500
+501        Returns:
+502            True if successful
+503
+504        Raises:
+505            APIError: If key doesn't exist
+506
+507        Examples:
+508            >>> async def doo_something():
+509            ...     async with AsyncOutlineClient(
+510            ...         "https://example.com:1234/secret",
+511            ...         "ab12cd34..."
+512            ...     ) as client:
+513            ...         # Rename key
+514            ...         await client.rename_access_key(1, "Alice")
+515            ...
+516            ...         # Verify new name
+517            ...         key = await client.get_access_key(1)
+518            ...         assert key.name == "Alice"
+519        """
+520        return await self._request(
+521            "PUT", f"access-keys/{key_id}/name", json={"name": name}
+522        )
+523
+524    async def delete_access_key(self, key_id: int) -> bool:
+525        """
+526        Delete access key.
+527
+528        Args:
+529            key_id: Access key ID
+530
+531        Returns:
+532            True if successful
+533
+534        Raises:
+535            APIError: If key doesn't exist
+536
+537        Examples:
+538            >>> async def doo_something():
+539            ...     async with AsyncOutlineClient(
+540            ...         "https://example.com:1234/secret",
+541            ...         "ab12cd34..."
+542            ...     ) as client:
+543            ...         if await client.delete_access_key(1):
+544            ...             print("Key deleted")
+545
+546        """
+547        return await self._request("DELETE", f"access-keys/{key_id}")
+548
+549    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
+550        """
+551        Set data transfer limit for access key.
+552
+553        Args:
+554            key_id: Access key ID
+555            bytes_limit: Limit in bytes (must be positive)
+556
+557        Returns:
+558            True if successful
+559
+560        Raises:
+561            APIError: If key doesn't exist or limit is invalid
+562
+563        Examples:
+564            >>> async def doo_something():
+565            ...     async with AsyncOutlineClient(
+566            ...         "https://example.com:1234/secret",
+567            ...         "ab12cd34..."
+568            ...     ) as client:
+569            ...         # Set 5 GB limit
+570            ...         limit = 5 * 1024**3  # 5 GB in bytes
+571            ...         await client.set_access_key_data_limit(1, limit)
+572            ...
+573            ...         # Verify limit
+574            ...         key = await client.get_access_key(1)
+575            ...         assert key.data_limit and key.data_limit.bytes == limit
+576        """
+577        return await self._request(
+578            "PUT",
+579            f"access-keys/{key_id}/data-limit",
+580            json={"limit": {"bytes": bytes_limit}},
+581        )
+582
+583    async def remove_access_key_data_limit(self, key_id: str) -> bool:
+584        """
+585        Remove data transfer limit from access key.
+586
+587        Args:
+588            key_id: Access key ID
+589
+590        Returns:
+591            True if successful
+592
+593        Raises:
+594            APIError: If key doesn't exist
+595        """
+596        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
+
+ + +

Asynchronous client for the Outline VPN Server API.

+ +
Arguments:
+ +
    +
  • api_url: Base URL for the Outline server API
  • +
  • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
  • +
  • json_format: Return raw JSON instead of Pydantic models
  • +
  • timeout: Request timeout in seconds
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         server_info = await client.get_server_info()
+
+
+
+
+ + +
+ +
+ + AsyncOutlineClient( api_url: str, cert_sha256: str, *, json_format: bool = True, timeout: float = 30.0) + + + +
+ +
56    def __init__(
+57        self,
+58        api_url: str,
+59        cert_sha256: str,
+60        *,
+61        json_format: bool = True,
+62        timeout: float = 30.0,
+63    ) -> None:
+64        self._api_url = api_url.rstrip("/")
+65        self._cert_sha256 = cert_sha256
+66        self._json_format = json_format
+67        self._timeout = aiohttp.ClientTimeout(total=timeout)
+68        self._ssl_context = None
+69        self._session: Optional[aiohttp.ClientSession] = None
+70        self._in_context = False
+
+ + + + +
+
+ +
+ + async def + get_server_info(self) -> Union[dict[str, Any], Server]: + + + +
+ +
216    async def get_server_info(self) -> Union[JsonDict, Server]:
+217        """
+218        Get server information.
+219
+220        Returns:
+221            Server information including name, ID, and configuration.
+222
+223        Examples:
+224            >>> async def doo_something():
+225            ...     async with AsyncOutlineClient(
+226            ...         "https://example.com:1234/secret",
+227            ...         "ab12cd34..."
+228            ...     ) as client:
+229            ...         server = await client.get_server_info()
+230            ...         print(f"Server {server.name} running version {server.version}")
+231        """
+232        response = await self._request("GET", "server")
+233        return await self._parse_response(
+234            response, Server, json_format=self._json_format
+235        )
+
+ + +

Get server information.

+ +
Returns:
+ +
+

Server information including name, ID, and configuration.

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         server = await client.get_server_info()
+...         print(f"Server {server.name} running version {server.version}")
+
+
+
+
+ + +
+
+ +
+ + async def + rename_server(self, name: str) -> bool: + + + +
+ +
237    async def rename_server(self, name: str) -> bool:
+238        """
+239        Rename the server.
+240
+241        Args:
+242            name: New server name
+243
+244        Returns:
+245            True if successful
+246
+247        Examples:
+248            >>> async def doo_something():
+249            ...     async with AsyncOutlineClient(
+250            ...         "https://example.com:1234/secret",
+251            ...         "ab12cd34..."
+252            ...     ) as client:
+253            ...     success = await client.rename_server("My VPN Server")
+254            ...     if success:
+255            ...         print("Server renamed successfully")
+256        """
+257        return await self._request("PUT", "name", json={"name": name})
+
+ + +

Rename the server.

+ +
Arguments:
+ +
    +
  • name: New server name
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...     success = await client.rename_server("My VPN Server")
+...     if success:
+...         print("Server renamed successfully")
+
+
+
+
+ + +
+
+ +
+ + async def + set_hostname(self, hostname: str) -> bool: + + + +
+ +
259    async def set_hostname(self, hostname: str) -> bool:
+260        """
+261        Set server hostname for access keys.
+262
+263        Args:
+264            hostname: New hostname or IP address
+265
+266        Returns:
+267            True if successful
+268
+269        Raises:
+270            APIError: If hostname is invalid
+271
+272        Examples:
+273            >>> async def doo_something():
+274            ...     async with AsyncOutlineClient(
+275            ...         "https://example.com:1234/secret",
+276            ...         "ab12cd34..."
+277            ...     ) as client:
+278            ...         await client.set_hostname("vpn.example.com")
+279            ...         # Or use IP address
+280            ...         await client.set_hostname("203.0.113.1")
+281        """
+282        return await self._request(
+283            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
+284        )
+
+ + +

Set server hostname for access keys.

+ +
Arguments:
+ +
    +
  • hostname: New hostname or IP address
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If hostname is invalid
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         await client.set_hostname("vpn.example.com")
+...         # Or use IP address
+...         await client.set_hostname("203.0.113.1")
+
+
+
+
+ + +
+
+ +
+ + async def + set_default_port(self, port: int) -> bool: + + + +
+ +
286    async def set_default_port(self, port: int) -> bool:
+287        """
+288        Set default port for new access keys.
+289
+290        Args:
+291            port: Port number (1025-65535)
+292
+293        Returns:
+294            True if successful
+295
+296        Raises:
+297            APIError: If port is invalid or in use
+298
+299        Examples:
+300            >>> async def doo_something():
+301            ...     async with AsyncOutlineClient(
+302            ...         "https://example.com:1234/secret",
+303            ...         "ab12cd34..."
+304            ...     ) as client:
+305            ...         await client.set_default_port(8388)
+306
+307        """
+308        return await self._request(
+309            "PUT", "server/port-for-new-access-keys", json={"port": port}
+310        )
+
+ + +

Set default port for new access keys.

+ +
Arguments:
+ +
    +
  • port: Port number (1025-65535)
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If port is invalid or in use
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         await client.set_default_port(8388)
+
+
+
+
+ + +
+
+ +
+ + async def + get_metrics_status(self) -> dict[str, typing.Any] | pydantic.main.BaseModel: + + + +
+ +
312    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
+313        """
+314        Get whether metrics collection is enabled.
+315
+316        Returns:
+317            Current metrics collection status
+318
+319        Examples:
+320            >>> async def doo_something():
+321            ...     async with AsyncOutlineClient(
+322            ...         "https://example.com:1234/secret",
+323            ...         "ab12cd34..."
+324            ...     ) as client:
+325            ...         if await client.get_metrics_status():
+326            ...             print("Metrics collection is enabled")
+327        """
+328        response = await self._request("GET", "metrics/enabled")
+329        data = await self._parse_response(
+330            response, MetricsStatusResponse, json_format=self._json_format
+331        )
+332        return data
+
+ + +

Get whether metrics collection is enabled.

+ +
Returns:
+ +
+

Current metrics collection status

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         if await client.get_metrics_status():
+...             print("Metrics collection is enabled")
+
+
+
+
+ + +
+
+ +
+ + async def + set_metrics_status(self, enabled: bool) -> bool: + + + +
+ +
334    async def set_metrics_status(self, enabled: bool) -> bool:
+335        """
+336        Enable or disable metrics collection.
+337
+338        Args:
+339            enabled: Whether to enable metrics
+340
+341        Returns:
+342            True if successful
+343
+344        Examples:
+345            >>> async def doo_something():
+346            ...     async with AsyncOutlineClient(
+347            ...         "https://example.com:1234/secret",
+348            ...         "ab12cd34..."
+349            ...     ) as client:
+350            ...         # Enable metrics
+351            ...         await client.set_metrics_status(True)
+352            ...         # Check new status
+353            ...         is_enabled = await client.get_metrics_status()
+354        """
+355        return await self._request(
+356            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
+357        )
+
+ + +

Enable or disable metrics collection.

+ +
Arguments:
+ +
    +
  • enabled: Whether to enable metrics
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Enable metrics
+...         await client.set_metrics_status(True)
+...         # Check new status
+...         is_enabled = await client.get_metrics_status()
+
+
+
+
+ + +
+
+ +
+ + async def + get_transfer_metrics( self, period: MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], ServerMetrics]: + + + +
+ +
359    async def get_transfer_metrics(
+360        self, period: MetricsPeriod = MetricsPeriod.MONTHLY
+361    ) -> Union[JsonDict, ServerMetrics]:
+362        """
+363        Get transfer metrics for specified period.
+364
+365        Args:
+366            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
+367
+368        Returns:
+369            Transfer metrics data for each access key
+370
+371        Examples:
+372            >>> async def doo_something():
+373            ...     async with AsyncOutlineClient(
+374            ...         "https://example.com:1234/secret",
+375            ...         "ab12cd34..."
+376            ...     ) as client:
+377            ...         # Get monthly metrics
+378            ...         metrics = await client.get_transfer_metrics()
+379            ...         # Or get daily metrics
+380            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+381            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+382            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+383        """
+384        response = await self._request(
+385            "GET", "metrics/transfer", params={"period": period.value}
+386        )
+387        return await self._parse_response(
+388            response, ServerMetrics, json_format=self._json_format
+389        )
+
+ + +

Get transfer metrics for specified period.

+ +
Arguments:
+ +
    +
  • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
  • +
+ +
Returns:
+ +
+

Transfer metrics data for each access key

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Get monthly metrics
+...         metrics = await client.get_transfer_metrics()
+...         # Or get daily metrics
+...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
+...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
+...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
+
+
+
+
+ + +
+
+ +
+ + async def + create_access_key( self, *, name: Optional[str] = None, password: Optional[str] = None, port: Optional[int] = None, method: Optional[str] = None, limit: Optional[DataLimit] = None) -> Union[dict[str, Any], AccessKey]: + + + +
+ +
391    async def create_access_key(
+392        self,
+393        *,
+394        name: Optional[str] = None,
+395        password: Optional[str] = None,
+396        port: Optional[int] = None,
+397        method: Optional[str] = None,
+398        limit: Optional[DataLimit] = None,
+399    ) -> Union[JsonDict, AccessKey]:
+400        """
+401        Create a new access key.
+402
+403        Args:
+404            name: Optional key name
+405            password: Optional password
+406            port: Optional port number (1-65535)
+407            method: Optional encryption method
+408            limit: Optional data transfer limit
+409
+410        Returns:
+411            New access key details
+412
+413        Examples:
+414            >>> async def doo_something():
+415            ...     async with AsyncOutlineClient(
+416            ...         "https://example.com:1234/secret",
+417            ...         "ab12cd34..."
+418            ...     ) as client:
+419            ...         # Create basic key
+420            ...         key = await client.create_access_key(name="User 1")
+421            ...
+422            ...         # Create key with data limit
+423            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+424            ...         key = await client.create_access_key(
+425            ...             name="Limited User",
+426            ...             port=8388,
+427            ...             limit=_limit
+428            ...         )
+429            ...         print(f"Created key: {key.access_url}")
+430        """
+431        request = AccessKeyCreateRequest(
+432            name=name, password=password, port=port, method=method, limit=limit
+433        )
+434        response = await self._request(
+435            "POST", "access-keys", json=request.model_dump(exclude_none=True)
+436        )
+437        return await self._parse_response(
+438            response, AccessKey, json_format=self._json_format
+439        )
+
+ + +

Create a new access key.

+ +
Arguments:
+ +
    +
  • name: Optional key name
  • +
  • password: Optional password
  • +
  • port: Optional port number (1-65535)
  • +
  • method: Optional encryption method
  • +
  • limit: Optional data transfer limit
  • +
+ +
Returns:
+ +
+

New access key details

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Create basic key
+...         key = await client.create_access_key(name="User 1")
+...
+...         # Create key with data limit
+...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
+...         key = await client.create_access_key(
+...             name="Limited User",
+...             port=8388,
+...             limit=_limit
+...         )
+...         print(f"Created key: {key.access_url}")
+
+
+
+
+ + +
+
+ +
+ + async def + get_access_keys(self) -> Union[dict[str, Any], AccessKeyList]: + + + +
+ +
441    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
+442        """
+443        Get all access keys.
+444
+445        Returns:
+446            List of all access keys
+447
+448        Examples:
+449            >>> async def doo_something():
+450            ...     async with AsyncOutlineClient(
+451            ...         "https://example.com:1234/secret",
+452            ...         "ab12cd34..."
+453            ...     ) as client:
+454            ...         keys = await client.get_access_keys()
+455            ...         for key in keys.access_keys:
+456            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+457            ...             if key.data_limit:
+458            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+459        """
+460        response = await self._request("GET", "access-keys")
+461        return await self._parse_response(
+462            response, AccessKeyList, json_format=self._json_format
+463        )
+
+ + +

Get all access keys.

+ +
Returns:
+ +
+

List of all access keys

+
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         keys = await client.get_access_keys()
+...         for key in keys.access_keys:
+...             print(f"Key {key.id}: {key.name or 'unnamed'}")
+...             if key.data_limit:
+...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
+
+
+
+
+ + +
+
+ +
+ + async def + get_access_key( self, key_id: int) -> Union[dict[str, Any], AccessKey]: + + + +
+ +
465    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
+466        """
+467        Get specific access key.
+468
+469        Args:
+470            key_id: Access key ID
+471
+472        Returns:
+473            Access key details
+474
+475        Raises:
+476            APIError: If key doesn't exist
+477
+478        Examples:
+479            >>> async def doo_something():
+480            ...     async with AsyncOutlineClient(
+481            ...         "https://example.com:1234/secret",
+482            ...         "ab12cd34..."
+483            ...     ) as client:
+484            ...         key = await client.get_access_key(1)
+485            ...         print(f"Port: {key.port}")
+486            ...         print(f"URL: {key.access_url}")
+487        """
+488        response = await self._request("GET", f"access-keys/{key_id}")
+489        return await self._parse_response(
+490            response, AccessKey, json_format=self._json_format
+491        )
+
+ + +

Get specific access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

Access key details

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         key = await client.get_access_key(1)
+...         print(f"Port: {key.port}")
+...         print(f"URL: {key.access_url}")
+
+
+
+
+ + +
+
+ +
+ + async def + rename_access_key(self, key_id: int, name: str) -> bool: + + + +
+ +
493    async def rename_access_key(self, key_id: int, name: str) -> bool:
+494        """
+495        Rename access key.
+496
+497        Args:
+498            key_id: Access key ID
+499            name: New name
+500
+501        Returns:
+502            True if successful
+503
+504        Raises:
+505            APIError: If key doesn't exist
+506
+507        Examples:
+508            >>> async def doo_something():
+509            ...     async with AsyncOutlineClient(
+510            ...         "https://example.com:1234/secret",
+511            ...         "ab12cd34..."
+512            ...     ) as client:
+513            ...         # Rename key
+514            ...         await client.rename_access_key(1, "Alice")
+515            ...
+516            ...         # Verify new name
+517            ...         key = await client.get_access_key(1)
+518            ...         assert key.name == "Alice"
+519        """
+520        return await self._request(
+521            "PUT", f"access-keys/{key_id}/name", json={"name": name}
+522        )
+
+ + +

Rename access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
  • name: New name
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Rename key
+...         await client.rename_access_key(1, "Alice")
+...
+...         # Verify new name
+...         key = await client.get_access_key(1)
+...         assert key.name == "Alice"
+
+
+
+
+ + +
+
+ +
+ + async def + delete_access_key(self, key_id: int) -> bool: + + + +
+ +
524    async def delete_access_key(self, key_id: int) -> bool:
+525        """
+526        Delete access key.
+527
+528        Args:
+529            key_id: Access key ID
+530
+531        Returns:
+532            True if successful
+533
+534        Raises:
+535            APIError: If key doesn't exist
+536
+537        Examples:
+538            >>> async def doo_something():
+539            ...     async with AsyncOutlineClient(
+540            ...         "https://example.com:1234/secret",
+541            ...         "ab12cd34..."
+542            ...     ) as client:
+543            ...         if await client.delete_access_key(1):
+544            ...             print("Key deleted")
+545
+546        """
+547        return await self._request("DELETE", f"access-keys/{key_id}")
+
+ + +

Delete access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         if await client.delete_access_key(1):
+...             print("Key deleted")
+
+
+
+
+ + +
+
+ +
+ + async def + set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool: + + + +
+ +
549    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
+550        """
+551        Set data transfer limit for access key.
+552
+553        Args:
+554            key_id: Access key ID
+555            bytes_limit: Limit in bytes (must be positive)
+556
+557        Returns:
+558            True if successful
+559
+560        Raises:
+561            APIError: If key doesn't exist or limit is invalid
+562
+563        Examples:
+564            >>> async def doo_something():
+565            ...     async with AsyncOutlineClient(
+566            ...         "https://example.com:1234/secret",
+567            ...         "ab12cd34..."
+568            ...     ) as client:
+569            ...         # Set 5 GB limit
+570            ...         limit = 5 * 1024**3  # 5 GB in bytes
+571            ...         await client.set_access_key_data_limit(1, limit)
+572            ...
+573            ...         # Verify limit
+574            ...         key = await client.get_access_key(1)
+575            ...         assert key.data_limit and key.data_limit.bytes == limit
+576        """
+577        return await self._request(
+578            "PUT",
+579            f"access-keys/{key_id}/data-limit",
+580            json={"limit": {"bytes": bytes_limit}},
+581        )
+
+ + +

Set data transfer limit for access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
  • bytes_limit: Limit in bytes (must be positive)
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist or limit is invalid
  • +
+ +
Examples:
+ +
+
+
>>> async def doo_something():
+...     async with AsyncOutlineClient(
+...         "https://example.com:1234/secret",
+...         "ab12cd34..."
+...     ) as client:
+...         # Set 5 GB limit
+...         limit = 5 * 1024**3  # 5 GB in bytes
+...         await client.set_access_key_data_limit(1, limit)
+...
+...         # Verify limit
+...         key = await client.get_access_key(1)
+...         assert key.data_limit and key.data_limit.bytes == limit
+
+
+
+
+ + +
+
+ +
+ + async def + remove_access_key_data_limit(self, key_id: str) -> bool: + + + +
+ +
583    async def remove_access_key_data_limit(self, key_id: str) -> bool:
+584        """
+585        Remove data transfer limit from access key.
+586
+587        Args:
+588            key_id: Access key ID
+589
+590        Returns:
+591            True if successful
+592
+593        Raises:
+594            APIError: If key doesn't exist
+595        """
+596        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
+
+ + +

Remove data transfer limit from access key.

+ +
Arguments:
+ +
    +
  • key_id: Access key ID
  • +
+ +
Returns:
+ +
+

True if successful

+
+ +
Raises:
+ +
    +
  • APIError: If key doesn't exist
  • +
+
+ + +
+
+
+ +
+ + class + OutlineError(builtins.Exception): + + + +
+ +
25class OutlineError(Exception):
+26    """Base exception for Outline client errors."""
+
+ + +

Base exception for Outline client errors.

+
+ + +
+
+ +
+ + class + APIError(pyoutlineapi.OutlineError): + + + +
+ +
29class APIError(OutlineError):
+30    """Raised when API requests fail."""
+
+ + +

Raised when API requests fail.

+
+ + +
+
+ +
+ + class + AccessKey(pydantic.main.BaseModel): + + + +
+ +
28class AccessKey(BaseModel):
+29    """Access key details."""
+30
+31    id: int
+32    name: Optional[str] = None
+33    password: str
+34    port: int = Field(gt=0, lt=65536)
+35    method: str
+36    access_url: str = Field(alias="accessUrl")
+37    data_limit: Optional[DataLimit] = Field(None, alias="dataLimit")
+
+ + +

Access key details.

+
+ + +
+
+ id: int + + +
+ + + + +
+
+
+ name: Optional[str] + + +
+ + + + +
+
+
+ password: str + + +
+ + + + +
+
+
+ port: int + + +
+ + + + +
+
+
+ method: str + + +
+ + + + +
+
+
+ access_url: str + + +
+ + + + +
+
+
+ data_limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + AccessKeyCreateRequest(pydantic.main.BaseModel): + + + +
+ +
105class AccessKeyCreateRequest(BaseModel):
+106    """
+107    Request parameters for creating an access key.
+108    Per OpenAPI: /access-keys POST request body
+109    """
+110
+111    name: Optional[str] = None
+112    method: Optional[str] = None
+113    password: Optional[str] = None
+114    port: Optional[int] = Field(None, gt=0, lt=65536)
+115    limit: Optional[DataLimit] = None
+
+ + +

Request parameters for creating an access key. +Per OpenAPI: /access-keys POST request body

+
+ + +
+
+ name: Optional[str] + + +
+ + + + +
+
+
+ method: Optional[str] + + +
+ + + + +
+
+
+ password: Optional[str] + + +
+ + + + +
+
+
+ port: Optional[int] + + +
+ + + + +
+
+
+ limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + AccessKeyList(pydantic.main.BaseModel): + + + +
+ +
40class AccessKeyList(BaseModel):
+41    """List of access keys."""
+42
+43    access_keys: list[AccessKey] = Field(alias="accessKeys")
+
+ + +

List of access keys.

+
+ + +
+
+ access_keys: list[AccessKey] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + DataLimit(pydantic.main.BaseModel): + + + +
+ +
16class DataLimit(BaseModel):
+17    """Data transfer limit configuration."""
+18
+19    bytes: int = Field(gt=0)
+20
+21    @field_validator("bytes")
+22    def validate_bytes(cls, v: int) -> int:
+23        if v < 0:
+24            raise ValueError("bytes must be positive")
+25        return v
+
+ + +

Data transfer limit configuration.

+
+ + +
+
+ bytes: int + + +
+ + + + +
+
+ +
+
@field_validator('bytes')
+ + def + validate_bytes(cls, v: int) -> int: + + + +
+ +
21    @field_validator("bytes")
+22    def validate_bytes(cls, v: int) -> int:
+23        if v < 0:
+24            raise ValueError("bytes must be positive")
+25        return v
+
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ErrorResponse(pydantic.main.BaseModel): + + + +
+ +
124class ErrorResponse(BaseModel):
+125    """
+126    Error response structure
+127    Per OpenAPI: 404 and 400 responses
+128    """
+129
+130    code: str
+131    message: str
+
+ + +

Error response structure +Per OpenAPI: 404 and 400 responses

+
+ + +
+
+ code: str + + +
+ + + + +
+
+
+ message: str + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ExperimentalMetrics(pydantic.main.BaseModel): + + + +
+ +
79class ExperimentalMetrics(BaseModel):
+80    """
+81    Experimental metrics data structure
+82    Per OpenAPI: /experimental/server/metrics endpoint
+83    """
+84
+85    server: list[ServerMetric]
+86    access_keys: list[AccessKeyMetric] = Field(alias="accessKeys")
+
+ + +

Experimental metrics data structure +Per OpenAPI: /experimental/server/metrics endpoint

+
+ + +
+
+ server: list[pyoutlineapi.models.ServerMetric] + + +
+ + + + +
+
+
+ access_keys: list[pyoutlineapi.models.AccessKeyMetric] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + MetricsPeriod(builtins.str, enum.Enum): + + + +
+ +
 8class MetricsPeriod(str, Enum):
+ 9    """Time periods for metrics collection."""
+10
+11    DAILY = "daily"
+12    WEEKLY = "weekly"
+13    MONTHLY = "monthly"
+
+ + +

Time periods for metrics collection.

+
+ + +
+
+ DAILY = +<MetricsPeriod.DAILY: 'daily'> + + +
+ + + + +
+
+
+ WEEKLY = +<MetricsPeriod.WEEKLY: 'weekly'> + + +
+ + + + +
+
+
+ MONTHLY = +<MetricsPeriod.MONTHLY: 'monthly'> + + +
+ + + + +
+
+
+ +
+ + class + MetricsStatusResponse(pydantic.main.BaseModel): + + + +
+ +
118class MetricsStatusResponse(BaseModel):
+119    """Response for /metrics/enabled endpoint"""
+120
+121    metrics_enabled: bool = Field(alias="metricsEnabled")
+
+ + +

Response for /metrics/enabled endpoint

+
+ + +
+
+ metrics_enabled: bool + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + Server(pydantic.main.BaseModel): + + + +
+ +
 89class Server(BaseModel):
+ 90    """
+ 91    Server information.
+ 92    Per OpenAPI: /server endpoint schema
+ 93    """
+ 94
+ 95    name: str
+ 96    server_id: str = Field(alias="serverId")
+ 97    metrics_enabled: bool = Field(alias="metricsEnabled")
+ 98    created_timestamp_ms: int = Field(alias="createdTimestampMs")
+ 99    version: str
+100    port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536)
+101    hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys")
+102    access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit")
+
+ + +

Server information. +Per OpenAPI: /server endpoint schema

+
+ + +
+
+ name: str + + +
+ + + + +
+
+
+ server_id: str + + +
+ + + + +
+
+
+ metrics_enabled: bool + + +
+ + + + +
+
+
+ created_timestamp_ms: int + + +
+ + + + +
+
+
+ version: str + + +
+ + + + +
+
+
+ port_for_new_access_keys: int + + +
+ + + + +
+
+
+ hostname_for_access_keys: Optional[str] + + +
+ + + + +
+
+
+ access_key_data_limit: Optional[DataLimit] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ +
+ + class + ServerMetrics(pydantic.main.BaseModel): + + + +
+ +
46class ServerMetrics(BaseModel):
+47    """
+48    Server metrics data for data transferred per access key
+49    Per OpenAPI: /metrics/transfer endpoint
+50    """
+51
+52    bytes_transferred_by_user_id: dict[str, int] = Field(
+53        alias="bytesTransferredByUserId"
+54    )
+
+ + +

Server metrics data for data transferred per access key +Per OpenAPI: /metrics/transfer endpoint

+
+ + +
+
+ bytes_transferred_by_user_id: dict[str, int] + + +
+ + + + +
+
+
+ model_config: ClassVar[pydantic.config.ConfigDict] = +{} + + +
+ + +

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/search.js b/docs/search.js new file mode 100644 index 0000000..6fbb620 --- /dev/null +++ b/docs/search.js @@ -0,0 +1,46 @@ +window.pdocSearch = (function(){ +/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o

\n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

Asynchronous client for the Outline VPN Server API.

\n\n
Arguments:
\n\n
    \n
  • api_url: Base URL for the Outline server API
  • \n
  • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
  • \n
  • json_format: Return raw JSON instead of Pydantic models
  • \n
  • timeout: Request timeout in seconds
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server_info = await client.get_server_info()\n
\n
\n
\n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

\n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\tjson_format: bool = True,\ttimeout: float = 30.0)"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_info", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_info", "kind": "function", "doc": "

Get server information.

\n\n
Returns:
\n\n
\n

Server information including name, ID, and configuration.

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server = await client.get_server_info()\n...         print(f"Server {server.name} running version {server.version}")\n
\n
\n
\n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.Server]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_server", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_server", "kind": "function", "doc": "

Rename the server.

\n\n
Arguments:
\n\n
    \n
  • name: New server name
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...     success = await client.rename_server("My VPN Server")\n...     if success:\n...         print("Server renamed successfully")\n
\n
\n
\n", "signature": "(self, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_hostname", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_hostname", "kind": "function", "doc": "

Set server hostname for access keys.

\n\n
Arguments:
\n\n
    \n
  • hostname: New hostname or IP address
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If hostname is invalid
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_hostname("vpn.example.com")\n...         # Or use IP address\n...         await client.set_hostname("203.0.113.1")\n
\n
\n
\n", "signature": "(self, hostname: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_default_port", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_default_port", "kind": "function", "doc": "

Set default port for new access keys.

\n\n
Arguments:
\n\n
    \n
  • port: Port number (1025-65535)
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If port is invalid or in use
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_default_port(8388)\n
\n
\n
\n", "signature": "(self, port: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_metrics_status", "kind": "function", "doc": "

Get whether metrics collection is enabled.

\n\n
Returns:
\n\n
\n

Current metrics collection status

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.get_metrics_status():\n...             print("Metrics collection is enabled")\n
\n
\n
\n", "signature": "(self) -> dict[str, typing.Any] | pydantic.main.BaseModel:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_metrics_status", "kind": "function", "doc": "

Enable or disable metrics collection.

\n\n
Arguments:
\n\n
    \n
  • enabled: Whether to enable metrics
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Enable metrics\n...         await client.set_metrics_status(True)\n...         # Check new status\n...         is_enabled = await client.get_metrics_status()\n
\n
\n
\n", "signature": "(self, enabled: bool) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_transfer_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_transfer_metrics", "kind": "function", "doc": "

Get transfer metrics for specified period.

\n\n
Arguments:
\n\n
    \n
  • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
  • \n
\n\n
Returns:
\n\n
\n

Transfer metrics data for each access key

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Get monthly metrics\n...         metrics = await client.get_transfer_metrics()\n...         # Or get daily metrics\n...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)\n...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():\n...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")\n
\n
\n
\n", "signature": "(\tself,\tperiod: pyoutlineapi.models.MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], pyoutlineapi.models.ServerMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key", "kind": "function", "doc": "

Create a new access key.

\n\n
Arguments:
\n\n
    \n
  • name: Optional key name
  • \n
  • password: Optional password
  • \n
  • port: Optional port number (1-65535)
  • \n
  • method: Optional encryption method
  • \n
  • limit: Optional data transfer limit
  • \n
\n\n
Returns:
\n\n
\n

New access key details

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Create basic key\n...         key = await client.create_access_key(name="User 1")\n...\n...         # Create key with data limit\n...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB\n...         key = await client.create_access_key(\n...             name="Limited User",\n...             port=8388,\n...             limit=_limit\n...         )\n...         print(f"Created key: {key.access_url}")\n
\n
\n
\n", "signature": "(\tself,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_keys", "kind": "function", "doc": "

Get all access keys.

\n\n
Returns:
\n\n
\n

List of all access keys

\n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         keys = await client.get_access_keys()\n...         for key in keys.access_keys:\n...             print(f"Key {key.id}: {key.name or 'unnamed'}")\n...             if key.data_limit:\n...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")\n
\n
\n
\n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.AccessKeyList]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_key", "kind": "function", "doc": "

Get specific access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

Access key details

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.get_access_key(1)\n...         print(f"Port: {key.port}")\n...         print(f"URL: {key.access_url}")\n
\n
\n
\n", "signature": "(\tself,\tkey_id: int) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_access_key", "kind": "function", "doc": "

Rename access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
  • name: New name
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Rename key\n...         await client.rename_access_key(1, "Alice")\n...\n...         # Verify new name\n...         key = await client.get_access_key(1)\n...         assert key.name == "Alice"\n
\n
\n
\n", "signature": "(self, key_id: int, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.delete_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.delete_access_key", "kind": "function", "doc": "

Delete access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.delete_access_key(1):\n...             print("Key deleted")\n
\n
\n
\n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_access_key_data_limit", "kind": "function", "doc": "

Set data transfer limit for access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
  • bytes_limit: Limit in bytes (must be positive)
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist or limit is invalid
  • \n
\n\n
Examples:
\n\n
\n
\n
>>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 5 GB limit\n...         limit = 5 * 1024**3  # 5 GB in bytes\n...         await client.set_access_key_data_limit(1, limit)\n...\n...         # Verify limit\n...         key = await client.get_access_key(1)\n...         assert key.data_limit and key.data_limit.bytes == limit\n
\n
\n
\n", "signature": "(self, key_id: int, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_access_key_data_limit", "kind": "function", "doc": "

Remove data transfer limit from access key.

\n\n
Arguments:
\n\n
    \n
  • key_id: Access key ID
  • \n
\n\n
Returns:
\n\n
\n

True if successful

\n
\n\n
Raises:
\n\n
    \n
  • APIError: If key doesn't exist
  • \n
\n", "signature": "(self, key_id: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

Base exception for Outline client errors.

\n", "bases": "builtins.Exception"}, {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

Raised when API requests fail.

\n", "bases": "pyoutlineapi.client.OutlineError"}, {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

Access key details.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKey.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKey.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

Request parameters for creating an access key.\nPer OpenAPI: /access-keys POST request body

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

\n", "annotation": ": Optional[int]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

List of access keys.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.AccessKey]"}, {"fullname": "pyoutlineapi.AccessKeyList.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

Data transfer limit configuration.

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.DataLimit.validate_bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.validate_bytes", "kind": "function", "doc": "

\n", "signature": "(cls, v: int) -> int:", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

Error response structure\nPer OpenAPI: 404 and 400 responses

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.model_config", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

Experimental metrics data structure\nPer OpenAPI: /experimental/server/metrics endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.ServerMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

\n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsPeriod", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod", "kind": "class", "doc": "

Time periods for metrics collection.

\n", "bases": "builtins.str, enum.Enum"}, {"fullname": "pyoutlineapi.MetricsPeriod.DAILY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.DAILY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.DAILY: 'daily'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.WEEKLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.WEEKLY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.WEEKLY: 'weekly'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.MONTHLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.MONTHLY", "kind": "variable", "doc": "

\n", "default_value": "<MetricsPeriod.MONTHLY: 'monthly'>"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

Response for /metrics/enabled endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

\n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

Server information.\nPer OpenAPI: /server endpoint schema

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

\n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

\n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

\n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

\n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

\n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.Server.model_config", "modulename": "pyoutlineapi", "qualname": "Server.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

Server metrics data for data transferred per access key\nPer OpenAPI: /metrics/transfer endpoint

\n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

\n", "annotation": ": dict[str, int]"}, {"fullname": "pyoutlineapi.ServerMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.model_config", "kind": "variable", "doc": "

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}]; + + // mirrored in build-search-index.js (part 1) + // Also split on html tags. this is a cheap heuristic, but good enough. + elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); + + let searchIndex; + if (docs._isPrebuiltIndex) { + console.info("using precompiled search index"); + searchIndex = elasticlunr.Index.load(docs); + } else { + console.time("building search index"); + // mirrored in build-search-index.js (part 2) + searchIndex = elasticlunr(function () { + this.pipeline.remove(elasticlunr.stemmer); + this.pipeline.remove(elasticlunr.stopWordFilter); + this.addField("qualname"); + this.addField("fullname"); + this.addField("annotation"); + this.addField("default_value"); + this.addField("signature"); + this.addField("bases"); + this.addField("doc"); + this.setRef("fullname"); + }); + for (let doc of docs) { + searchIndex.addDoc(doc); + } + console.timeEnd("building search index"); + } + + return (term) => searchIndex.search(term, { + fields: { + qualname: {boost: 4}, + fullname: {boost: 2}, + annotation: {boost: 2}, + default_value: {boost: 2}, + signature: {boost: 2}, + bases: {boost: 2}, + doc: {boost: 1}, + }, + expand: true + }); +})(); \ No newline at end of file From 8b33493c5e4afd9a61aed6be9fef77fd46cfd34a Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 01:55:58 +0500 Subject: [PATCH 11/24] chore: some adjustment --- .gitignore | 114 ++++++++++++++++++++++++++++++++------------- poetry.lock | 123 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 5 +- 3 files changed, 207 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index bd51b7a..4c2e18c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,21 +7,28 @@ __pycache__/ dist/ build/ *.egg-info/ +MANIFEST # C extensions *.so -# Distribution / packaging +# Virtual environment .Python env/ venv/ ENV/ +.env/ +.venv/ env.bak/ venv.bak/ +pythonenv* +.python-version + +# Package files *.egg +*.whl # PyInstaller -# Usually these files are written by a python script from a .spec file *.manifest *.spec @@ -30,56 +37,99 @@ pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ .coverage +.coverage.* coverage.xml *.cover *.py,cover +.pytest_cache/ nosetests.xml test_*.xml -*.tox/ -*.nox/ -*.coverage -*.hypothesis/ -*.pytest_cache/ - -# Pytest -.cache +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django *.log +local_settings.py +db.sqlite3 +db.sqlite3-journal -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json +# Flask +instance/ +.webassets-cache -# IDEs and editors -.vscode/ -.idea/ -*.swp -*.swo +# Scrapy +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ # Jupyter Notebook .ipynb_checkpoints +*.ipynb -# Pyre type checker -.pyre/ +# IPython +profile_default/ +ipython_config.py -# Virtual environment -.venv/ +# pyenv + +# Celery +celerybeat-schedule +celerybeat.pid +# SageMath +*.sage.py -# Environment variables +# Environments .env .env.* +.venv -# macOS -.DS_Store +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre +.pyre/ -# Windows +# pytype +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IDE settings +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db Thumbs.db Desktop.ini -# Miscellaneous -*.orig -*.bak -*.tmp -/main.py +# Project specific +logs/ +tmp/ +temp/ + +main.py \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index bd31a43..0b19784 100644 --- a/poetry.lock +++ b/poetry.lock @@ -469,6 +469,95 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + [[package]] name = "multidict" version = "6.1.0" @@ -670,6 +759,23 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pdoc" +version = "15.0.1" +description = "API Documentation for Python Projects" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pdoc-15.0.1-py3-none-any.whl", hash = "sha256:fd437ab8eb55f9b942226af7865a3801e2fb731665199b74fd9a44737dbe20f9"}, + {file = "pdoc-15.0.1.tar.gz", hash = "sha256:3b08382c9d312243ee6c2a1813d0ff517a6ab84d596fa2c6c6b5255b17c3d666"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.0" +MarkupSafe = ">=1.1.1" +pygments = ">=2.12.0" + [[package]] name = "platformdirs" version = "4.3.6" @@ -929,6 +1035,21 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" version = "8.3.4" @@ -1172,4 +1293,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "f563f761b8c84386e5834219f9b2c9f0e1a87023436c13ce9337e6fa06962997" +content-hash = "0cf9f74293d58c162e15afc8a3627c83fa8a8de172466a15277eed65dfe17a55" diff --git a/pyproject.toml b/pyproject.toml index 5f92655..8369d3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,14 +18,14 @@ classifiers = [ "Operating System :: OS Independent", "Intended Audience :: Developers", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Security", + "Topic :: Internet :: Proxy Servers", "Programming Language :: Python :: 3 :: Only", "Typing :: Typed", "Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Framework :: aiohttp", "Framework :: Pydantic", - "Topic :: Security", - "Topic :: Internet :: Proxy Servers", ] [tool.poetry.dependencies] @@ -40,6 +40,7 @@ pytest-cov = "^5.0.0" black = "^24.10.0" mypy = "^1.0.0" ruff = "^0.3.0" +pdoc = "^15.0.1" [tool.pytest.ini_options] addopts = "--cov=pyoutlineapi --cov-report=term-missing --cov-report=xml --cov-report=html" From 324fcfac1bc0a7ee6d266f0005e5219d00dfa8c1 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 13:28:18 +0500 Subject: [PATCH 12/24] chore: some adjustment --- docs/pyoutlineapi.html | 2172 ++++++++++++++++++++------------------ docs/search.js | 2 +- pyoutlineapi/__init__.py | 68 +- pyoutlineapi/client.py | 176 +-- pyoutlineapi/models.py | 13 + pyproject.toml | 6 +- 6 files changed, 1303 insertions(+), 1134 deletions(-) diff --git a/docs/pyoutlineapi.html b/docs/pyoutlineapi.html index 68f939b..fdfbadb 100644 --- a/docs/pyoutlineapi.html +++ b/docs/pyoutlineapi.html @@ -83,6 +83,12 @@

API Documentation

  • APIError
  • @@ -285,42 +291,101 @@

    API Documentation

    pyoutlineapi

    - +

    PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    + +

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru +All rights reserved.

    + +

    This software is licensed under the MIT License.

    + +
    You can find the full license text at:
    + +
    +

    https://opensource.org/licenses/MIT

    +
    + +
    Source code repository:
    + +
    +

    https://github.com/orenlab/pyoutlineapi

    +
    +
    + -
     1from .client import AsyncOutlineClient, OutlineError, APIError
    - 2from .models import (
    - 3    AccessKey,
    - 4    AccessKeyCreateRequest,
    - 5    AccessKeyList,
    - 6    DataLimit,
    - 7    ErrorResponse,
    - 8    ExperimentalMetrics,
    - 9    MetricsPeriod,
    -10    MetricsStatusResponse,
    -11    Server,
    -12    ServerMetrics,
    -13)
    -14
    -15__version__ = "0.2.0"
    +                        
     1"""
    + 2PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.
    + 3
    + 4Copyright (c) 2025 Denis Rozhnovskiy <pytelemonbot@mail.ru>
    + 5All rights reserved.
    + 6
    + 7This software is licensed under the MIT License.
    + 8You can find the full license text at:
    + 9    https://opensource.org/licenses/MIT
    +10
    +11Source code repository:
    +12    https://github.com/orenlab/pyoutlineapi
    +13"""
    +14import sys
    +15from typing import TYPE_CHECKING
     16
    -17__all__ = [
    -18    "AsyncOutlineClient",
    -19    "OutlineError",
    -20    "APIError",
    -21    "AccessKey",
    -22    "AccessKeyCreateRequest",
    -23    "AccessKeyList",
    -24    "DataLimit",
    -25    "ErrorResponse",
    -26    "ExperimentalMetrics",
    -27    "MetricsPeriod",
    -28    "MetricsStatusResponse",
    -29    "Server",
    -30    "ServerMetrics",
    -31]
    +17if sys.version_info < (3, 10):
    +18    raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher")
    +19
    +20from .client import AsyncOutlineClient, OutlineError, APIError
    +21
    +22if TYPE_CHECKING:
    +23    from .models import (
    +24        AccessKey,
    +25        AccessKeyCreateRequest,
    +26        AccessKeyList,
    +27        DataLimit,
    +28        ErrorResponse,
    +29        ExperimentalMetrics,
    +30        MetricsPeriod,
    +31        MetricsStatusResponse,
    +32        Server,
    +33        ServerMetrics,
    +34    )
    +35
    +36__version__: str = "0.2.0"
    +37__author__ = "Denis Rozhnovskiy"
    +38__email__ = "pytelemonbot@mail.ru"
    +39__license__ = "MIT"
    +40
    +41PUBLIC_API = [
    +42    "AsyncOutlineClient",
    +43    "OutlineError",
    +44    "APIError",
    +45    "AccessKey",
    +46    "AccessKeyCreateRequest",
    +47    "AccessKeyList",
    +48    "DataLimit",
    +49    "ErrorResponse",
    +50    "ExperimentalMetrics",
    +51    "MetricsPeriod",
    +52    "MetricsStatusResponse",
    +53    "Server",
    +54    "ServerMetrics",
    +55]
    +56
    +57__all__ = PUBLIC_API
    +58
    +59# Actual imports for runtime
    +60from .models import (
    +61    AccessKey,
    +62    AccessKeyCreateRequest,
    +63    AccessKeyList,
    +64    DataLimit,
    +65    ErrorResponse,
    +66    ExperimentalMetrics,
    +67    MetricsPeriod,
    +68    MetricsStatusResponse,
    +69    Server,
    +70    ServerMetrics,
    +71)
     
    @@ -336,531 +401,497 @@

    -
     37class AsyncOutlineClient:
    - 38    """
    - 39    Asynchronous client for the Outline VPN Server API.
    - 40
    - 41    Args:
    - 42        api_url: Base URL for the Outline server API
    - 43        cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    - 44        json_format: Return raw JSON instead of Pydantic models
    - 45        timeout: Request timeout in seconds
    - 46
    - 47    Examples:
    - 48        >>> async def doo_something():
    - 49        ...     async with AsyncOutlineClient(
    - 50        ...         "https://example.com:1234/secret",
    - 51        ...         "ab12cd34..."
    - 52        ...     ) as client:
    - 53        ...         server_info = await client.get_server_info()
    - 54    """
    - 55
    - 56    def __init__(
    - 57        self,
    - 58        api_url: str,
    - 59        cert_sha256: str,
    - 60        *,
    - 61        json_format: bool = True,
    - 62        timeout: float = 30.0,
    - 63    ) -> None:
    - 64        self._api_url = api_url.rstrip("/")
    - 65        self._cert_sha256 = cert_sha256
    - 66        self._json_format = json_format
    - 67        self._timeout = aiohttp.ClientTimeout(total=timeout)
    - 68        self._ssl_context = None
    - 69        self._session: Optional[aiohttp.ClientSession] = None
    - 70        self._in_context = False
    - 71
    - 72    async def __aenter__(self) -> AsyncOutlineClient:
    - 73        """Set up client session for context manager."""
    - 74        self._session = aiohttp.ClientSession(
    - 75            timeout=self._timeout, raise_for_status=True
    - 76        )
    - 77        self._in_context = True
    - 78        return self
    - 79
    - 80    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    - 81        """Clean up client session."""
    - 82        if self._session:
    - 83            await self._session.close()
    - 84            self._session = None
    - 85        self._in_context = False
    - 86
    - 87    def _ensure_context(self):
    - 88        """Ensure the session context is valid."""
    - 89        if not self._session or self._session.closed:
    - 90            raise RuntimeError("Client session is not initialized or already closed.")
    - 91
    - 92    @overload
    - 93    async def _parse_response(
    - 94        self,
    - 95        response: ClientResponse,
    - 96        model: type[BaseModel],
    - 97        json_format: Literal[True],
    - 98    ) -> JsonDict: ...
    - 99
    -100    @overload
    -101    async def _parse_response(
    -102        self,
    -103        response: ClientResponse,
    -104        model: type[BaseModel],
    -105        json_format: Literal[False],
    -106    ) -> BaseModel: ...
    -107
    -108    @overload
    -109    async def _parse_response(
    -110        self, response: ClientResponse, model: type[BaseModel], json_format: bool
    -111    ) -> Union[JsonDict, BaseModel]: ...
    -112
    -113    async def _parse_response(
    -114        self, response: ClientResponse, model: type[BaseModel], json_format: bool = True
    -115    ) -> Union[JsonDict, BaseModel]:
    -116        """
    -117        Parse and validate API response data.
    -118
    -119        Args:
    -120            response: API response to parse
    -121            model: Pydantic model for validation
    -122            json_format: Whether to return raw JSON
    -123
    -124        Returns:
    -125            Validated response data
    -126
    -127        Raises:
    -128            ValueError: If response validation fails
    -129        """
    -130        self._ensure_context()
    -131
    -132        try:
    -133            data = await response.json()
    -134        except aiohttp.ContentTypeError:
    -135            raise ValueError("Invalid response format") from None
    -136        try:
    -137            validated = model.model_validate(data)
    -138            return validated.model_dump() if json_format else validated
    -139        except Exception as e:
    -140            raise ValueError(f"Value error: {e}") from e
    -141
    -142    @staticmethod
    -143    async def _handle_error_response(response: ClientResponse) -> None:
    -144        """Handle error responses from the API."""
    -145        try:
    -146            error_data = await response.json()
    -147            error = ErrorResponse.model_validate(error_data)
    -148            raise APIError(f"{error.code}: {error.message}")
    -149        except ValueError:
    -150            raise APIError(f"HTTP {response.status}: {response.reason}")
    -151
    -152    async def _request(
    -153        self,
    -154        method: str,
    -155        endpoint: str,
    -156        *,
    -157        json: Any = None,
    -158        params: Optional[dict[str, Any]] = None,
    -159    ) -> Any:
    -160        """Make an API request."""
    -161        self._ensure_context()
    -162
    -163        url = self._build_url(endpoint)
    -164        ssl_context = self._get_ssl_context()
    -165
    -166        async with self._session.request(
    -167            method,
    -168            url,
    -169            json=json,
    -170            params=params,
    -171            ssl=ssl_context,
    -172            raise_for_status=False,
    -173            timeout=self._timeout,
    -174        ) as response:
    -175            if response.status >= 400:
    -176                await self._handle_error_response(response)
    -177
    -178            if response.status == 204:
    -179                return True  # No content response
    -180
    -181            try:
    -182                await response.json()
    -183                return response
    -184            except aiohttp.ContentTypeError:
    -185                return await response.text()  # Fallback for non-JSON responses
    -186            except Exception as e:
    -187                raise APIError(f"Failed to parse response from {url}: {e}") from e
    -188
    -189    def _build_url(self, endpoint: str) -> str:
    -190        """Build and validate the full URL for the API request."""
    -191        if not isinstance(endpoint, str):
    -192            raise ValueError("Endpoint must be a string")
    -193
    -194        endpoint = endpoint.lstrip("/")
    -195        url = f"{self._api_url}/{endpoint}"
    -196
    -197        parsed_url = urlparse(url)
    -198        if not parsed_url.scheme or not parsed_url.netloc:
    -199            raise ValueError(f"Invalid URL: {url}")
    -200
    -201        return url
    -202
    -203    def _get_ssl_context(self) -> Optional[Fingerprint]:
    -204        """Create an SSL context if a certificate fingerprint is provided."""
    -205        if not self._cert_sha256:
    -206            return None
    -207
    -208        try:
    -209            fingerprint = binascii.unhexlify(self._cert_sha256)
    -210            return Fingerprint(fingerprint)
    -211        except binascii.Error as e:
    -212            raise ValueError(f"Invalid certificate SHA256: {self._cert_sha256}") from e
    -213        except Exception as e:
    -214            raise OutlineError("Error while creating SSL context") from e
    -215
    -216    async def get_server_info(self) -> Union[JsonDict, Server]:
    -217        """
    -218        Get server information.
    -219
    -220        Returns:
    -221            Server information including name, ID, and configuration.
    -222
    -223        Examples:
    -224            >>> async def doo_something():
    -225            ...     async with AsyncOutlineClient(
    -226            ...         "https://example.com:1234/secret",
    -227            ...         "ab12cd34..."
    -228            ...     ) as client:
    -229            ...         server = await client.get_server_info()
    -230            ...         print(f"Server {server.name} running version {server.version}")
    -231        """
    -232        response = await self._request("GET", "server")
    -233        return await self._parse_response(
    -234            response, Server, json_format=self._json_format
    -235        )
    -236
    -237    async def rename_server(self, name: str) -> bool:
    -238        """
    -239        Rename the server.
    -240
    -241        Args:
    -242            name: New server name
    -243
    -244        Returns:
    -245            True if successful
    -246
    -247        Examples:
    -248            >>> async def doo_something():
    -249            ...     async with AsyncOutlineClient(
    -250            ...         "https://example.com:1234/secret",
    -251            ...         "ab12cd34..."
    -252            ...     ) as client:
    -253            ...     success = await client.rename_server("My VPN Server")
    -254            ...     if success:
    -255            ...         print("Server renamed successfully")
    -256        """
    -257        return await self._request("PUT", "name", json={"name": name})
    -258
    -259    async def set_hostname(self, hostname: str) -> bool:
    -260        """
    -261        Set server hostname for access keys.
    +            
     71class AsyncOutlineClient:
    + 72    """
    + 73    Asynchronous client for the Outline VPN Server API.
    + 74
    + 75    Args:
    + 76        api_url: Base URL for the Outline server API
    + 77        cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    + 78        json_format: Return raw JSON instead of Pydantic models
    + 79        timeout: Request timeout in seconds
    + 80
    + 81    Examples:
    + 82        >>> async def doo_something():
    + 83        ...     async with AsyncOutlineClient(
    + 84        ...         "https://example.com:1234/secret",
    + 85        ...         "ab12cd34..."
    + 86        ...     ) as client:
    + 87        ...         server_info = await client.get_server_info()
    + 88    """
    + 89
    + 90    def __init__(
    + 91            self,
    + 92            api_url: str,
    + 93            cert_sha256: str,
    + 94            *,
    + 95            json_format: bool = True,
    + 96            timeout: float = 30.0,
    + 97    ) -> None:
    + 98        self._api_url = api_url.rstrip("/")
    + 99        self._cert_sha256 = cert_sha256
    +100        self._json_format = json_format
    +101        self._timeout = aiohttp.ClientTimeout(total=timeout)
    +102        self._ssl_context: Optional[Fingerprint] = None
    +103        self._session: Optional[aiohttp.ClientSession] = None
    +104
    +105    async def __aenter__(self) -> AsyncOutlineClient:
    +106        """Set up client session for context manager."""
    +107        self._session = aiohttp.ClientSession(
    +108            timeout=self._timeout,
    +109            raise_for_status=False,
    +110            connector=aiohttp.TCPConnector(ssl=self._get_ssl_context())
    +111        )
    +112        return self
    +113
    +114    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    +115        """Clean up client session."""
    +116        if self._session:
    +117            await self._session.close()
    +118            self._session = None
    +119
    +120    @overload
    +121    async def _parse_response(
    +122            self,
    +123            response: ClientResponse,
    +124            model: type[BaseModel],
    +125            json_format: Literal[True],
    +126    ) -> JsonDict:
    +127        ...
    +128
    +129    @overload
    +130    async def _parse_response(
    +131            self,
    +132            response: ClientResponse,
    +133            model: type[BaseModel],
    +134            json_format: Literal[False],
    +135    ) -> BaseModel:
    +136        ...
    +137
    +138    @overload
    +139    async def _parse_response(
    +140            self, response: ClientResponse, model: type[BaseModel], json_format: bool
    +141    ) -> Union[JsonDict, BaseModel]:
    +142        ...
    +143
    +144    @ensure_context
    +145    async def _parse_response(
    +146            self,
    +147            response: ClientResponse,
    +148            model: type[BaseModel],
    +149            json_format: bool = True
    +150    ) -> ResponseType:
    +151        """
    +152        Parse and validate API response data.
    +153
    +154        Args:
    +155            response: API response to parse
    +156            model: Pydantic model for validation
    +157            json_format: Whether to return raw JSON
    +158
    +159        Returns:
    +160            Validated response data
    +161
    +162        Raises:
    +163            ValueError: If response validation fails
    +164        """
    +165        try:
    +166            data = await response.json()
    +167            validated = model.model_validate(data)
    +168            return validated.model_dump() if json_format else validated
    +169        except aiohttp.ContentTypeError as e:
    +170            raise ValueError("Invalid response format") from e
    +171        except Exception as e:
    +172            raise ValueError(f"Validation error: {e}") from e
    +173
    +174    @staticmethod
    +175    async def _handle_error_response(response: ClientResponse) -> None:
    +176        """Handle error responses from the API."""
    +177        try:
    +178            error_data = await response.json()
    +179            error = ErrorResponse.model_validate(error_data)
    +180            raise APIError(f"{error.code}: {error.message}", response.status)
    +181        except ValueError:
    +182            raise APIError(f"HTTP {response.status}: {response.reason}", response.status)
    +183
    +184    @ensure_context
    +185    async def _request(
    +186            self,
    +187            method: str,
    +188            endpoint: str,
    +189            *,
    +190            json: Any = None,
    +191            params: Optional[dict[str, Any]] = None,
    +192    ) -> Any:
    +193        """Make an API request."""
    +194        url = self._build_url(endpoint)
    +195
    +196        async with self._session.request(
    +197                method,
    +198                url,
    +199                json=json,
    +200                params=params,
    +201                raise_for_status=False,
    +202        ) as response:
    +203            if response.status >= 400:
    +204                await self._handle_error_response(response)
    +205
    +206            if response.status == 204:
    +207                return True
    +208
    +209            try:
    +210                await response.json()
    +211                return response
    +212            except aiohttp.ContentTypeError:
    +213                return await response.text()
    +214            except Exception as e:
    +215                raise APIError(f"Failed to parse response: {e}", response.status)
    +216
    +217    def _build_url(self, endpoint: str) -> str:
    +218        """Build and validate the full URL for the API request."""
    +219        if not isinstance(endpoint, str):
    +220            raise ValueError("Endpoint must be a string")
    +221
    +222        url = f"{self._api_url}/{endpoint.lstrip('/')}"
    +223        parsed_url = urlparse(url)
    +224
    +225        if not all([parsed_url.scheme, parsed_url.netloc]):
    +226            raise ValueError(f"Invalid URL: {url}")
    +227
    +228        return url
    +229
    +230    def _get_ssl_context(self) -> Optional[Fingerprint]:
    +231        """Create an SSL context if a certificate fingerprint is provided."""
    +232        if not self._cert_sha256:
    +233            return None
    +234
    +235        try:
    +236            return Fingerprint(binascii.unhexlify(self._cert_sha256))
    +237        except binascii.Error as e:
    +238            raise ValueError(f"Invalid certificate SHA256: {self._cert_sha256}") from e
    +239        except Exception as e:
    +240            raise OutlineError("Failed to create SSL context") from e
    +241
    +242    async def get_server_info(self) -> Union[JsonDict, Server]:
    +243        """
    +244        Get server information.
    +245
    +246        Returns:
    +247            Server information including name, ID, and configuration.
    +248
    +249        Examples:
    +250            >>> async def doo_something():
    +251            ...     async with AsyncOutlineClient(
    +252            ...         "https://example.com:1234/secret",
    +253            ...         "ab12cd34..."
    +254            ...     ) as client:
    +255            ...         server = await client.get_server_info()
    +256            ...         print(f"Server {server.name} running version {server.version}")
    +257        """
    +258        response = await self._request("GET", "server")
    +259        return await self._parse_response(
    +260            response, Server, json_format=self._json_format
    +261        )
     262
    -263        Args:
    -264            hostname: New hostname or IP address
    -265
    -266        Returns:
    -267            True if successful
    -268
    -269        Raises:
    -270            APIError: If hostname is invalid
    -271
    -272        Examples:
    -273            >>> async def doo_something():
    -274            ...     async with AsyncOutlineClient(
    -275            ...         "https://example.com:1234/secret",
    -276            ...         "ab12cd34..."
    -277            ...     ) as client:
    -278            ...         await client.set_hostname("vpn.example.com")
    -279            ...         # Or use IP address
    -280            ...         await client.set_hostname("203.0.113.1")
    -281        """
    -282        return await self._request(
    -283            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
    -284        )
    -285
    -286    async def set_default_port(self, port: int) -> bool:
    -287        """
    -288        Set default port for new access keys.
    -289
    -290        Args:
    -291            port: Port number (1025-65535)
    -292
    -293        Returns:
    -294            True if successful
    -295
    -296        Raises:
    -297            APIError: If port is invalid or in use
    -298
    -299        Examples:
    -300            >>> async def doo_something():
    -301            ...     async with AsyncOutlineClient(
    -302            ...         "https://example.com:1234/secret",
    -303            ...         "ab12cd34..."
    -304            ...     ) as client:
    -305            ...         await client.set_default_port(8388)
    -306
    +263    async def rename_server(self, name: str) -> bool:
    +264        """
    +265        Rename the server.
    +266
    +267        Args:
    +268            name: New server name
    +269
    +270        Returns:
    +271            True if successful
    +272
    +273        Examples:
    +274            >>> async def doo_something():
    +275            ...     async with AsyncOutlineClient(
    +276            ...         "https://example.com:1234/secret",
    +277            ...         "ab12cd34..."
    +278            ...     ) as client:
    +279            ...     success = await client.rename_server("My VPN Server")
    +280            ...     if success:
    +281            ...         print("Server renamed successfully")
    +282        """
    +283        return await self._request("PUT", "name", json={"name": name})
    +284
    +285    async def set_hostname(self, hostname: str) -> bool:
    +286        """
    +287        Set server hostname for access keys.
    +288
    +289        Args:
    +290            hostname: New hostname or IP address
    +291
    +292        Returns:
    +293            True if successful
    +294
    +295        Raises:
    +296            APIError: If hostname is invalid
    +297
    +298        Examples:
    +299            >>> async def doo_something():
    +300            ...     async with AsyncOutlineClient(
    +301            ...         "https://example.com:1234/secret",
    +302            ...         "ab12cd34..."
    +303            ...     ) as client:
    +304            ...         await client.set_hostname("vpn.example.com")
    +305            ...         # Or use IP address
    +306            ...         await client.set_hostname("203.0.113.1")
     307        """
     308        return await self._request(
    -309            "PUT", "server/port-for-new-access-keys", json={"port": port}
    +309            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
     310        )
     311
    -312    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
    +312    async def set_default_port(self, port: int) -> bool:
     313        """
    -314        Get whether metrics collection is enabled.
    +314        Set default port for new access keys.
     315
    -316        Returns:
    -317            Current metrics collection status
    +316        Args:
    +317            port: Port number (1025-65535)
     318
    -319        Examples:
    -320            >>> async def doo_something():
    -321            ...     async with AsyncOutlineClient(
    -322            ...         "https://example.com:1234/secret",
    -323            ...         "ab12cd34..."
    -324            ...     ) as client:
    -325            ...         if await client.get_metrics_status():
    -326            ...             print("Metrics collection is enabled")
    -327        """
    -328        response = await self._request("GET", "metrics/enabled")
    -329        data = await self._parse_response(
    -330            response, MetricsStatusResponse, json_format=self._json_format
    -331        )
    -332        return data
    -333
    -334    async def set_metrics_status(self, enabled: bool) -> bool:
    -335        """
    -336        Enable or disable metrics collection.
    +319        Returns:
    +320            True if successful
    +321
    +322        Raises:
    +323            APIError: If port is invalid or in use
    +324
    +325        Examples:
    +326            >>> async def doo_something():
    +327            ...     async with AsyncOutlineClient(
    +328            ...         "https://example.com:1234/secret",
    +329            ...         "ab12cd34..."
    +330            ...     ) as client:
    +331            ...         await client.set_default_port(8388)
    +332
    +333        """
    +334        return await self._request(
    +335            "PUT", "server/port-for-new-access-keys", json={"port": port}
    +336        )
     337
    -338        Args:
    -339            enabled: Whether to enable metrics
    -340
    -341        Returns:
    -342            True if successful
    -343
    -344        Examples:
    -345            >>> async def doo_something():
    -346            ...     async with AsyncOutlineClient(
    -347            ...         "https://example.com:1234/secret",
    -348            ...         "ab12cd34..."
    -349            ...     ) as client:
    -350            ...         # Enable metrics
    -351            ...         await client.set_metrics_status(True)
    -352            ...         # Check new status
    -353            ...         is_enabled = await client.get_metrics_status()
    -354        """
    -355        return await self._request(
    -356            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
    +338    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
    +339        """
    +340        Get whether metrics collection is enabled.
    +341
    +342        Returns:
    +343            Current metrics collection status
    +344
    +345        Examples:
    +346            >>> async def doo_something():
    +347            ...     async with AsyncOutlineClient(
    +348            ...         "https://example.com:1234/secret",
    +349            ...         "ab12cd34..."
    +350            ...     ) as client:
    +351            ...         if await client.get_metrics_status():
    +352            ...             print("Metrics collection is enabled")
    +353        """
    +354        response = await self._request("GET", "metrics/enabled")
    +355        data = await self._parse_response(
    +356            response, MetricsStatusResponse, json_format=self._json_format
     357        )
    -358
    -359    async def get_transfer_metrics(
    -360        self, period: MetricsPeriod = MetricsPeriod.MONTHLY
    -361    ) -> Union[JsonDict, ServerMetrics]:
    -362        """
    -363        Get transfer metrics for specified period.
    -364
    -365        Args:
    -366            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    -367
    -368        Returns:
    -369            Transfer metrics data for each access key
    -370
    -371        Examples:
    -372            >>> async def doo_something():
    -373            ...     async with AsyncOutlineClient(
    -374            ...         "https://example.com:1234/secret",
    -375            ...         "ab12cd34..."
    -376            ...     ) as client:
    -377            ...         # Get monthly metrics
    -378            ...         metrics = await client.get_transfer_metrics()
    -379            ...         # Or get daily metrics
    -380            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
    -381            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
    -382            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    -383        """
    -384        response = await self._request(
    -385            "GET", "metrics/transfer", params={"period": period.value}
    -386        )
    -387        return await self._parse_response(
    -388            response, ServerMetrics, json_format=self._json_format
    -389        )
    +358        return data
    +359
    +360    async def set_metrics_status(self, enabled: bool) -> bool:
    +361        """
    +362        Enable or disable metrics collection.
    +363
    +364        Args:
    +365            enabled: Whether to enable metrics
    +366
    +367        Returns:
    +368            True if successful
    +369
    +370        Examples:
    +371            >>> async def doo_something():
    +372            ...     async with AsyncOutlineClient(
    +373            ...         "https://example.com:1234/secret",
    +374            ...         "ab12cd34..."
    +375            ...     ) as client:
    +376            ...         # Enable metrics
    +377            ...         await client.set_metrics_status(True)
    +378            ...         # Check new status
    +379            ...         is_enabled = await client.get_metrics_status()
    +380        """
    +381        return await self._request(
    +382            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
    +383        )
    +384
    +385    async def get_transfer_metrics(
    +386            self, period: MetricsPeriod = MetricsPeriod.MONTHLY
    +387    ) -> Union[JsonDict, ServerMetrics]:
    +388        """
    +389        Get transfer metrics for specified period.
     390
    -391    async def create_access_key(
    -392        self,
    -393        *,
    -394        name: Optional[str] = None,
    -395        password: Optional[str] = None,
    -396        port: Optional[int] = None,
    -397        method: Optional[str] = None,
    -398        limit: Optional[DataLimit] = None,
    -399    ) -> Union[JsonDict, AccessKey]:
    -400        """
    -401        Create a new access key.
    -402
    -403        Args:
    -404            name: Optional key name
    -405            password: Optional password
    -406            port: Optional port number (1-65535)
    -407            method: Optional encryption method
    -408            limit: Optional data transfer limit
    -409
    -410        Returns:
    -411            New access key details
    -412
    -413        Examples:
    -414            >>> async def doo_something():
    -415            ...     async with AsyncOutlineClient(
    -416            ...         "https://example.com:1234/secret",
    -417            ...         "ab12cd34..."
    -418            ...     ) as client:
    -419            ...         # Create basic key
    -420            ...         key = await client.create_access_key(name="User 1")
    -421            ...
    -422            ...         # Create key with data limit
    -423            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
    -424            ...         key = await client.create_access_key(
    -425            ...             name="Limited User",
    -426            ...             port=8388,
    -427            ...             limit=_limit
    -428            ...         )
    -429            ...         print(f"Created key: {key.access_url}")
    -430        """
    -431        request = AccessKeyCreateRequest(
    -432            name=name, password=password, port=port, method=method, limit=limit
    -433        )
    -434        response = await self._request(
    -435            "POST", "access-keys", json=request.model_dump(exclude_none=True)
    -436        )
    -437        return await self._parse_response(
    -438            response, AccessKey, json_format=self._json_format
    -439        )
    -440
    -441    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
    -442        """
    -443        Get all access keys.
    -444
    -445        Returns:
    -446            List of all access keys
    -447
    -448        Examples:
    -449            >>> async def doo_something():
    -450            ...     async with AsyncOutlineClient(
    -451            ...         "https://example.com:1234/secret",
    -452            ...         "ab12cd34..."
    -453            ...     ) as client:
    -454            ...         keys = await client.get_access_keys()
    -455            ...         for key in keys.access_keys:
    -456            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    -457            ...             if key.data_limit:
    -458            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    -459        """
    -460        response = await self._request("GET", "access-keys")
    -461        return await self._parse_response(
    -462            response, AccessKeyList, json_format=self._json_format
    -463        )
    -464
    -465    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
    -466        """
    -467        Get specific access key.
    -468
    -469        Args:
    -470            key_id: Access key ID
    -471
    -472        Returns:
    -473            Access key details
    -474
    -475        Raises:
    -476            APIError: If key doesn't exist
    -477
    -478        Examples:
    -479            >>> async def doo_something():
    -480            ...     async with AsyncOutlineClient(
    -481            ...         "https://example.com:1234/secret",
    -482            ...         "ab12cd34..."
    -483            ...     ) as client:
    -484            ...         key = await client.get_access_key(1)
    -485            ...         print(f"Port: {key.port}")
    -486            ...         print(f"URL: {key.access_url}")
    -487        """
    -488        response = await self._request("GET", f"access-keys/{key_id}")
    -489        return await self._parse_response(
    -490            response, AccessKey, json_format=self._json_format
    -491        )
    -492
    -493    async def rename_access_key(self, key_id: int, name: str) -> bool:
    -494        """
    -495        Rename access key.
    -496
    -497        Args:
    -498            key_id: Access key ID
    -499            name: New name
    +391        Args:
    +392            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    +393
    +394        Returns:
    +395            Transfer metrics data for each access key
    +396
    +397        Examples:
    +398            >>> async def doo_something():
    +399            ...     async with AsyncOutlineClient(
    +400            ...         "https://example.com:1234/secret",
    +401            ...         "ab12cd34..."
    +402            ...     ) as client:
    +403            ...         # Get monthly metrics
    +404            ...         metrics = await client.get_transfer_metrics()
    +405            ...         # Or get daily metrics
    +406            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
    +407            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
    +408            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    +409        """
    +410        response = await self._request(
    +411            "GET", "metrics/transfer", params={"period": period.value}
    +412        )
    +413        return await self._parse_response(
    +414            response, ServerMetrics, json_format=self._json_format
    +415        )
    +416
    +417    async def create_access_key(
    +418            self,
    +419            *,
    +420            name: Optional[str] = None,
    +421            password: Optional[str] = None,
    +422            port: Optional[int] = None,
    +423            method: Optional[str] = None,
    +424            limit: Optional[DataLimit] = None,
    +425    ) -> Union[JsonDict, AccessKey]:
    +426        """
    +427        Create a new access key.
    +428
    +429        Args:
    +430            name: Optional key name
    +431            password: Optional password
    +432            port: Optional port number (1-65535)
    +433            method: Optional encryption method
    +434            limit: Optional data transfer limit
    +435
    +436        Returns:
    +437            New access key details
    +438
    +439        Examples:
    +440            >>> async def doo_something():
    +441            ...     async with AsyncOutlineClient(
    +442            ...         "https://example.com:1234/secret",
    +443            ...         "ab12cd34..."
    +444            ...     ) as client:
    +445            ...         # Create basic key
    +446            ...         key = await client.create_access_key(name="User 1")
    +447            ...
    +448            ...         # Create key with data limit
    +449            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
    +450            ...         key = await client.create_access_key(
    +451            ...             name="Limited User",
    +452            ...             port=8388,
    +453            ...             limit=_limit
    +454            ...         )
    +455            ...         print(f"Created key: {key.access_url}")
    +456        """
    +457        request = AccessKeyCreateRequest(
    +458            name=name, password=password, port=port, method=method, limit=limit
    +459        )
    +460        response = await self._request(
    +461            "POST", "access-keys", json=request.model_dump(exclude_none=True)
    +462        )
    +463        return await self._parse_response(
    +464            response, AccessKey, json_format=self._json_format
    +465        )
    +466
    +467    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
    +468        """
    +469        Get all access keys.
    +470
    +471        Returns:
    +472            List of all access keys
    +473
    +474        Examples:
    +475            >>> async def doo_something():
    +476            ...     async with AsyncOutlineClient(
    +477            ...         "https://example.com:1234/secret",
    +478            ...         "ab12cd34..."
    +479            ...     ) as client:
    +480            ...         keys = await client.get_access_keys()
    +481            ...         for key in keys.access_keys:
    +482            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    +483            ...             if key.data_limit:
    +484            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    +485        """
    +486        response = await self._request("GET", "access-keys")
    +487        return await self._parse_response(
    +488            response, AccessKeyList, json_format=self._json_format
    +489        )
    +490
    +491    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
    +492        """
    +493        Get specific access key.
    +494
    +495        Args:
    +496            key_id: Access key ID
    +497
    +498        Returns:
    +499            Access key details
     500
    -501        Returns:
    -502            True if successful
    +501        Raises:
    +502            APIError: If key doesn't exist
     503
    -504        Raises:
    -505            APIError: If key doesn't exist
    -506
    -507        Examples:
    -508            >>> async def doo_something():
    -509            ...     async with AsyncOutlineClient(
    -510            ...         "https://example.com:1234/secret",
    -511            ...         "ab12cd34..."
    -512            ...     ) as client:
    -513            ...         # Rename key
    -514            ...         await client.rename_access_key(1, "Alice")
    -515            ...
    -516            ...         # Verify new name
    -517            ...         key = await client.get_access_key(1)
    -518            ...         assert key.name == "Alice"
    -519        """
    -520        return await self._request(
    -521            "PUT", f"access-keys/{key_id}/name", json={"name": name}
    -522        )
    -523
    -524    async def delete_access_key(self, key_id: int) -> bool:
    -525        """
    -526        Delete access key.
    -527
    -528        Args:
    -529            key_id: Access key ID
    -530
    -531        Returns:
    -532            True if successful
    -533
    -534        Raises:
    -535            APIError: If key doesn't exist
    -536
    -537        Examples:
    -538            >>> async def doo_something():
    -539            ...     async with AsyncOutlineClient(
    -540            ...         "https://example.com:1234/secret",
    -541            ...         "ab12cd34..."
    -542            ...     ) as client:
    -543            ...         if await client.delete_access_key(1):
    -544            ...             print("Key deleted")
    -545
    -546        """
    -547        return await self._request("DELETE", f"access-keys/{key_id}")
    -548
    -549    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
    -550        """
    -551        Set data transfer limit for access key.
    -552
    -553        Args:
    -554            key_id: Access key ID
    -555            bytes_limit: Limit in bytes (must be positive)
    +504        Examples:
    +505            >>> async def doo_something():
    +506            ...     async with AsyncOutlineClient(
    +507            ...         "https://example.com:1234/secret",
    +508            ...         "ab12cd34..."
    +509            ...     ) as client:
    +510            ...         key = await client.get_access_key(1)
    +511            ...         print(f"Port: {key.port}")
    +512            ...         print(f"URL: {key.access_url}")
    +513        """
    +514        response = await self._request("GET", f"access-keys/{key_id}")
    +515        return await self._parse_response(
    +516            response, AccessKey, json_format=self._json_format
    +517        )
    +518
    +519    async def rename_access_key(self, key_id: int, name: str) -> bool:
    +520        """
    +521        Rename access key.
    +522
    +523        Args:
    +524            key_id: Access key ID
    +525            name: New name
    +526
    +527        Returns:
    +528            True if successful
    +529
    +530        Raises:
    +531            APIError: If key doesn't exist
    +532
    +533        Examples:
    +534            >>> async def doo_something():
    +535            ...     async with AsyncOutlineClient(
    +536            ...         "https://example.com:1234/secret",
    +537            ...         "ab12cd34..."
    +538            ...     ) as client:
    +539            ...         # Rename key
    +540            ...         await client.rename_access_key(1, "Alice")
    +541            ...
    +542            ...         # Verify new name
    +543            ...         key = await client.get_access_key(1)
    +544            ...         assert key.name == "Alice"
    +545        """
    +546        return await self._request(
    +547            "PUT", f"access-keys/{key_id}/name", json={"name": name}
    +548        )
    +549
    +550    async def delete_access_key(self, key_id: int) -> bool:
    +551        """
    +552        Delete access key.
    +553
    +554        Args:
    +555            key_id: Access key ID
     556
     557        Returns:
     558            True if successful
     559
     560        Raises:
    -561            APIError: If key doesn't exist or limit is invalid
    +561            APIError: If key doesn't exist
     562
     563        Examples:
     564            >>> async def doo_something():
    @@ -868,34 +899,60 @@ 

    566 ... "https://example.com:1234/secret", 567 ... "ab12cd34..." 568 ... ) as client: -569 ... # Set 5 GB limit -570 ... limit = 5 * 1024**3 # 5 GB in bytes -571 ... await client.set_access_key_data_limit(1, limit) -572 ... -573 ... # Verify limit -574 ... key = await client.get_access_key(1) -575 ... assert key.data_limit and key.data_limit.bytes == limit -576 """ -577 return await self._request( -578 "PUT", -579 f"access-keys/{key_id}/data-limit", -580 json={"limit": {"bytes": bytes_limit}}, -581 ) +569 ... if await client.delete_access_key(1): +570 ... print("Key deleted") +571 +572 """ +573 return await self._request("DELETE", f"access-keys/{key_id}") +574 +575 async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool: +576 """ +577 Set data transfer limit for access key. +578 +579 Args: +580 key_id: Access key ID +581 bytes_limit: Limit in bytes (must be positive) 582 -583 async def remove_access_key_data_limit(self, key_id: str) -> bool: -584 """ -585 Remove data transfer limit from access key. -586 -587 Args: -588 key_id: Access key ID -589 -590 Returns: -591 True if successful -592 -593 Raises: -594 APIError: If key doesn't exist -595 """ -596 return await self._request("DELETE", f"access-keys/{key_id}/data-limit") +583 Returns: +584 True if successful +585 +586 Raises: +587 APIError: If key doesn't exist or limit is invalid +588 +589 Examples: +590 >>> async def doo_something(): +591 ... async with AsyncOutlineClient( +592 ... "https://example.com:1234/secret", +593 ... "ab12cd34..." +594 ... ) as client: +595 ... # Set 5 GB limit +596 ... limit = 5 * 1024**3 # 5 GB in bytes +597 ... await client.set_access_key_data_limit(1, limit) +598 ... +599 ... # Verify limit +600 ... key = await client.get_access_key(1) +601 ... assert key.data_limit and key.data_limit.bytes == limit +602 """ +603 return await self._request( +604 "PUT", +605 f"access-keys/{key_id}/data-limit", +606 json={"limit": {"bytes": bytes_limit}}, +607 ) +608 +609 async def remove_access_key_data_limit(self, key_id: str) -> bool: +610 """ +611 Remove data transfer limit from access key. +612 +613 Args: +614 key_id: Access key ID +615 +616 Returns: +617 True if successful +618 +619 Raises: +620 APIError: If key doesn't exist +621 """ +622 return await self._request("DELETE", f"access-keys/{key_id}/data-limit")

    @@ -936,21 +993,20 @@
    Examples:
    -
    56    def __init__(
    -57        self,
    -58        api_url: str,
    -59        cert_sha256: str,
    -60        *,
    -61        json_format: bool = True,
    -62        timeout: float = 30.0,
    -63    ) -> None:
    -64        self._api_url = api_url.rstrip("/")
    -65        self._cert_sha256 = cert_sha256
    -66        self._json_format = json_format
    -67        self._timeout = aiohttp.ClientTimeout(total=timeout)
    -68        self._ssl_context = None
    -69        self._session: Optional[aiohttp.ClientSession] = None
    -70        self._in_context = False
    +            
     90    def __init__(
    + 91            self,
    + 92            api_url: str,
    + 93            cert_sha256: str,
    + 94            *,
    + 95            json_format: bool = True,
    + 96            timeout: float = 30.0,
    + 97    ) -> None:
    + 98        self._api_url = api_url.rstrip("/")
    + 99        self._cert_sha256 = cert_sha256
    +100        self._json_format = json_format
    +101        self._timeout = aiohttp.ClientTimeout(total=timeout)
    +102        self._ssl_context: Optional[Fingerprint] = None
    +103        self._session: Optional[aiohttp.ClientSession] = None
     
    @@ -968,26 +1024,26 @@
    Examples:
    -
    216    async def get_server_info(self) -> Union[JsonDict, Server]:
    -217        """
    -218        Get server information.
    -219
    -220        Returns:
    -221            Server information including name, ID, and configuration.
    -222
    -223        Examples:
    -224            >>> async def doo_something():
    -225            ...     async with AsyncOutlineClient(
    -226            ...         "https://example.com:1234/secret",
    -227            ...         "ab12cd34..."
    -228            ...     ) as client:
    -229            ...         server = await client.get_server_info()
    -230            ...         print(f"Server {server.name} running version {server.version}")
    -231        """
    -232        response = await self._request("GET", "server")
    -233        return await self._parse_response(
    -234            response, Server, json_format=self._json_format
    -235        )
    +            
    242    async def get_server_info(self) -> Union[JsonDict, Server]:
    +243        """
    +244        Get server information.
    +245
    +246        Returns:
    +247            Server information including name, ID, and configuration.
    +248
    +249        Examples:
    +250            >>> async def doo_something():
    +251            ...     async with AsyncOutlineClient(
    +252            ...         "https://example.com:1234/secret",
    +253            ...         "ab12cd34..."
    +254            ...     ) as client:
    +255            ...         server = await client.get_server_info()
    +256            ...         print(f"Server {server.name} running version {server.version}")
    +257        """
    +258        response = await self._request("GET", "server")
    +259        return await self._parse_response(
    +260            response, Server, json_format=self._json_format
    +261        )
     
    @@ -1028,27 +1084,27 @@
    Examples:
    -
    237    async def rename_server(self, name: str) -> bool:
    -238        """
    -239        Rename the server.
    -240
    -241        Args:
    -242            name: New server name
    -243
    -244        Returns:
    -245            True if successful
    -246
    -247        Examples:
    -248            >>> async def doo_something():
    -249            ...     async with AsyncOutlineClient(
    -250            ...         "https://example.com:1234/secret",
    -251            ...         "ab12cd34..."
    -252            ...     ) as client:
    -253            ...     success = await client.rename_server("My VPN Server")
    -254            ...     if success:
    -255            ...         print("Server renamed successfully")
    -256        """
    -257        return await self._request("PUT", "name", json={"name": name})
    +            
    263    async def rename_server(self, name: str) -> bool:
    +264        """
    +265        Rename the server.
    +266
    +267        Args:
    +268            name: New server name
    +269
    +270        Returns:
    +271            True if successful
    +272
    +273        Examples:
    +274            >>> async def doo_something():
    +275            ...     async with AsyncOutlineClient(
    +276            ...         "https://example.com:1234/secret",
    +277            ...         "ab12cd34..."
    +278            ...     ) as client:
    +279            ...     success = await client.rename_server("My VPN Server")
    +280            ...     if success:
    +281            ...         print("Server renamed successfully")
    +282        """
    +283        return await self._request("PUT", "name", json={"name": name})
     
    @@ -1096,32 +1152,32 @@
    Examples:
    -
    259    async def set_hostname(self, hostname: str) -> bool:
    -260        """
    -261        Set server hostname for access keys.
    -262
    -263        Args:
    -264            hostname: New hostname or IP address
    -265
    -266        Returns:
    -267            True if successful
    -268
    -269        Raises:
    -270            APIError: If hostname is invalid
    -271
    -272        Examples:
    -273            >>> async def doo_something():
    -274            ...     async with AsyncOutlineClient(
    -275            ...         "https://example.com:1234/secret",
    -276            ...         "ab12cd34..."
    -277            ...     ) as client:
    -278            ...         await client.set_hostname("vpn.example.com")
    -279            ...         # Or use IP address
    -280            ...         await client.set_hostname("203.0.113.1")
    -281        """
    -282        return await self._request(
    -283            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
    -284        )
    +            
    285    async def set_hostname(self, hostname: str) -> bool:
    +286        """
    +287        Set server hostname for access keys.
    +288
    +289        Args:
    +290            hostname: New hostname or IP address
    +291
    +292        Returns:
    +293            True if successful
    +294
    +295        Raises:
    +296            APIError: If hostname is invalid
    +297
    +298        Examples:
    +299            >>> async def doo_something():
    +300            ...     async with AsyncOutlineClient(
    +301            ...         "https://example.com:1234/secret",
    +302            ...         "ab12cd34..."
    +303            ...     ) as client:
    +304            ...         await client.set_hostname("vpn.example.com")
    +305            ...         # Or use IP address
    +306            ...         await client.set_hostname("203.0.113.1")
    +307        """
    +308        return await self._request(
    +309            "PUT", "server/hostname-for-access-keys", json={"hostname": hostname}
    +310        )
     
    @@ -1175,31 +1231,31 @@
    Examples:
    -
    286    async def set_default_port(self, port: int) -> bool:
    -287        """
    -288        Set default port for new access keys.
    -289
    -290        Args:
    -291            port: Port number (1025-65535)
    -292
    -293        Returns:
    -294            True if successful
    -295
    -296        Raises:
    -297            APIError: If port is invalid or in use
    -298
    -299        Examples:
    -300            >>> async def doo_something():
    -301            ...     async with AsyncOutlineClient(
    -302            ...         "https://example.com:1234/secret",
    -303            ...         "ab12cd34..."
    -304            ...     ) as client:
    -305            ...         await client.set_default_port(8388)
    -306
    -307        """
    -308        return await self._request(
    -309            "PUT", "server/port-for-new-access-keys", json={"port": port}
    -310        )
    +            
    312    async def set_default_port(self, port: int) -> bool:
    +313        """
    +314        Set default port for new access keys.
    +315
    +316        Args:
    +317            port: Port number (1025-65535)
    +318
    +319        Returns:
    +320            True if successful
    +321
    +322        Raises:
    +323            APIError: If port is invalid or in use
    +324
    +325        Examples:
    +326            >>> async def doo_something():
    +327            ...     async with AsyncOutlineClient(
    +328            ...         "https://example.com:1234/secret",
    +329            ...         "ab12cd34..."
    +330            ...     ) as client:
    +331            ...         await client.set_default_port(8388)
    +332
    +333        """
    +334        return await self._request(
    +335            "PUT", "server/port-for-new-access-keys", json={"port": port}
    +336        )
     
    @@ -1251,27 +1307,27 @@
    Examples:
    -
    312    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
    -313        """
    -314        Get whether metrics collection is enabled.
    -315
    -316        Returns:
    -317            Current metrics collection status
    -318
    -319        Examples:
    -320            >>> async def doo_something():
    -321            ...     async with AsyncOutlineClient(
    -322            ...         "https://example.com:1234/secret",
    -323            ...         "ab12cd34..."
    -324            ...     ) as client:
    -325            ...         if await client.get_metrics_status():
    -326            ...             print("Metrics collection is enabled")
    -327        """
    -328        response = await self._request("GET", "metrics/enabled")
    -329        data = await self._parse_response(
    -330            response, MetricsStatusResponse, json_format=self._json_format
    -331        )
    -332        return data
    +            
    338    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
    +339        """
    +340        Get whether metrics collection is enabled.
    +341
    +342        Returns:
    +343            Current metrics collection status
    +344
    +345        Examples:
    +346            >>> async def doo_something():
    +347            ...     async with AsyncOutlineClient(
    +348            ...         "https://example.com:1234/secret",
    +349            ...         "ab12cd34..."
    +350            ...     ) as client:
    +351            ...         if await client.get_metrics_status():
    +352            ...             print("Metrics collection is enabled")
    +353        """
    +354        response = await self._request("GET", "metrics/enabled")
    +355        data = await self._parse_response(
    +356            response, MetricsStatusResponse, json_format=self._json_format
    +357        )
    +358        return data
     
    @@ -1312,30 +1368,30 @@
    Examples:
    -
    334    async def set_metrics_status(self, enabled: bool) -> bool:
    -335        """
    -336        Enable or disable metrics collection.
    -337
    -338        Args:
    -339            enabled: Whether to enable metrics
    -340
    -341        Returns:
    -342            True if successful
    -343
    -344        Examples:
    -345            >>> async def doo_something():
    -346            ...     async with AsyncOutlineClient(
    -347            ...         "https://example.com:1234/secret",
    -348            ...         "ab12cd34..."
    -349            ...     ) as client:
    -350            ...         # Enable metrics
    -351            ...         await client.set_metrics_status(True)
    -352            ...         # Check new status
    -353            ...         is_enabled = await client.get_metrics_status()
    -354        """
    -355        return await self._request(
    -356            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
    -357        )
    +            
    360    async def set_metrics_status(self, enabled: bool) -> bool:
    +361        """
    +362        Enable or disable metrics collection.
    +363
    +364        Args:
    +365            enabled: Whether to enable metrics
    +366
    +367        Returns:
    +368            True if successful
    +369
    +370        Examples:
    +371            >>> async def doo_something():
    +372            ...     async with AsyncOutlineClient(
    +373            ...         "https://example.com:1234/secret",
    +374            ...         "ab12cd34..."
    +375            ...     ) as client:
    +376            ...         # Enable metrics
    +377            ...         await client.set_metrics_status(True)
    +378            ...         # Check new status
    +379            ...         is_enabled = await client.get_metrics_status()
    +380        """
    +381        return await self._request(
    +382            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
    +383        )
     
    @@ -1384,37 +1440,37 @@
    Examples:
    -
    359    async def get_transfer_metrics(
    -360        self, period: MetricsPeriod = MetricsPeriod.MONTHLY
    -361    ) -> Union[JsonDict, ServerMetrics]:
    -362        """
    -363        Get transfer metrics for specified period.
    -364
    -365        Args:
    -366            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    -367
    -368        Returns:
    -369            Transfer metrics data for each access key
    -370
    -371        Examples:
    -372            >>> async def doo_something():
    -373            ...     async with AsyncOutlineClient(
    -374            ...         "https://example.com:1234/secret",
    -375            ...         "ab12cd34..."
    -376            ...     ) as client:
    -377            ...         # Get monthly metrics
    -378            ...         metrics = await client.get_transfer_metrics()
    -379            ...         # Or get daily metrics
    -380            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
    -381            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
    -382            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    -383        """
    -384        response = await self._request(
    -385            "GET", "metrics/transfer", params={"period": period.value}
    -386        )
    -387        return await self._parse_response(
    -388            response, ServerMetrics, json_format=self._json_format
    -389        )
    +            
    385    async def get_transfer_metrics(
    +386            self, period: MetricsPeriod = MetricsPeriod.MONTHLY
    +387    ) -> Union[JsonDict, ServerMetrics]:
    +388        """
    +389        Get transfer metrics for specified period.
    +390
    +391        Args:
    +392            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    +393
    +394        Returns:
    +395            Transfer metrics data for each access key
    +396
    +397        Examples:
    +398            >>> async def doo_something():
    +399            ...     async with AsyncOutlineClient(
    +400            ...         "https://example.com:1234/secret",
    +401            ...         "ab12cd34..."
    +402            ...     ) as client:
    +403            ...         # Get monthly metrics
    +404            ...         metrics = await client.get_transfer_metrics()
    +405            ...         # Or get daily metrics
    +406            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
    +407            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
    +408            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    +409        """
    +410        response = await self._request(
    +411            "GET", "metrics/transfer", params={"period": period.value}
    +412        )
    +413        return await self._parse_response(
    +414            response, ServerMetrics, json_format=self._json_format
    +415        )
     
    @@ -1465,55 +1521,55 @@
    Examples:
    -
    391    async def create_access_key(
    -392        self,
    -393        *,
    -394        name: Optional[str] = None,
    -395        password: Optional[str] = None,
    -396        port: Optional[int] = None,
    -397        method: Optional[str] = None,
    -398        limit: Optional[DataLimit] = None,
    -399    ) -> Union[JsonDict, AccessKey]:
    -400        """
    -401        Create a new access key.
    -402
    -403        Args:
    -404            name: Optional key name
    -405            password: Optional password
    -406            port: Optional port number (1-65535)
    -407            method: Optional encryption method
    -408            limit: Optional data transfer limit
    -409
    -410        Returns:
    -411            New access key details
    -412
    -413        Examples:
    -414            >>> async def doo_something():
    -415            ...     async with AsyncOutlineClient(
    -416            ...         "https://example.com:1234/secret",
    -417            ...         "ab12cd34..."
    -418            ...     ) as client:
    -419            ...         # Create basic key
    -420            ...         key = await client.create_access_key(name="User 1")
    -421            ...
    -422            ...         # Create key with data limit
    -423            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
    -424            ...         key = await client.create_access_key(
    -425            ...             name="Limited User",
    -426            ...             port=8388,
    -427            ...             limit=_limit
    -428            ...         )
    -429            ...         print(f"Created key: {key.access_url}")
    -430        """
    -431        request = AccessKeyCreateRequest(
    -432            name=name, password=password, port=port, method=method, limit=limit
    -433        )
    -434        response = await self._request(
    -435            "POST", "access-keys", json=request.model_dump(exclude_none=True)
    -436        )
    -437        return await self._parse_response(
    -438            response, AccessKey, json_format=self._json_format
    -439        )
    +            
    417    async def create_access_key(
    +418            self,
    +419            *,
    +420            name: Optional[str] = None,
    +421            password: Optional[str] = None,
    +422            port: Optional[int] = None,
    +423            method: Optional[str] = None,
    +424            limit: Optional[DataLimit] = None,
    +425    ) -> Union[JsonDict, AccessKey]:
    +426        """
    +427        Create a new access key.
    +428
    +429        Args:
    +430            name: Optional key name
    +431            password: Optional password
    +432            port: Optional port number (1-65535)
    +433            method: Optional encryption method
    +434            limit: Optional data transfer limit
    +435
    +436        Returns:
    +437            New access key details
    +438
    +439        Examples:
    +440            >>> async def doo_something():
    +441            ...     async with AsyncOutlineClient(
    +442            ...         "https://example.com:1234/secret",
    +443            ...         "ab12cd34..."
    +444            ...     ) as client:
    +445            ...         # Create basic key
    +446            ...         key = await client.create_access_key(name="User 1")
    +447            ...
    +448            ...         # Create key with data limit
    +449            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
    +450            ...         key = await client.create_access_key(
    +451            ...             name="Limited User",
    +452            ...             port=8388,
    +453            ...             limit=_limit
    +454            ...         )
    +455            ...         print(f"Created key: {key.access_url}")
    +456        """
    +457        request = AccessKeyCreateRequest(
    +458            name=name, password=password, port=port, method=method, limit=limit
    +459        )
    +460        response = await self._request(
    +461            "POST", "access-keys", json=request.model_dump(exclude_none=True)
    +462        )
    +463        return await self._parse_response(
    +464            response, AccessKey, json_format=self._json_format
    +465        )
     
    @@ -1573,29 +1629,29 @@
    Examples:
    -
    441    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
    -442        """
    -443        Get all access keys.
    -444
    -445        Returns:
    -446            List of all access keys
    -447
    -448        Examples:
    -449            >>> async def doo_something():
    -450            ...     async with AsyncOutlineClient(
    -451            ...         "https://example.com:1234/secret",
    -452            ...         "ab12cd34..."
    -453            ...     ) as client:
    -454            ...         keys = await client.get_access_keys()
    -455            ...         for key in keys.access_keys:
    -456            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    -457            ...             if key.data_limit:
    -458            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    -459        """
    -460        response = await self._request("GET", "access-keys")
    -461        return await self._parse_response(
    -462            response, AccessKeyList, json_format=self._json_format
    -463        )
    +            
    467    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
    +468        """
    +469        Get all access keys.
    +470
    +471        Returns:
    +472            List of all access keys
    +473
    +474        Examples:
    +475            >>> async def doo_something():
    +476            ...     async with AsyncOutlineClient(
    +477            ...         "https://example.com:1234/secret",
    +478            ...         "ab12cd34..."
    +479            ...     ) as client:
    +480            ...         keys = await client.get_access_keys()
    +481            ...         for key in keys.access_keys:
    +482            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    +483            ...             if key.data_limit:
    +484            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    +485        """
    +486        response = await self._request("GET", "access-keys")
    +487        return await self._parse_response(
    +488            response, AccessKeyList, json_format=self._json_format
    +489        )
     
    @@ -1639,33 +1695,33 @@
    Examples:
    -
    465    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
    -466        """
    -467        Get specific access key.
    -468
    -469        Args:
    -470            key_id: Access key ID
    -471
    -472        Returns:
    -473            Access key details
    -474
    -475        Raises:
    -476            APIError: If key doesn't exist
    -477
    -478        Examples:
    -479            >>> async def doo_something():
    -480            ...     async with AsyncOutlineClient(
    -481            ...         "https://example.com:1234/secret",
    -482            ...         "ab12cd34..."
    -483            ...     ) as client:
    -484            ...         key = await client.get_access_key(1)
    -485            ...         print(f"Port: {key.port}")
    -486            ...         print(f"URL: {key.access_url}")
    -487        """
    -488        response = await self._request("GET", f"access-keys/{key_id}")
    -489        return await self._parse_response(
    -490            response, AccessKey, json_format=self._json_format
    -491        )
    +            
    491    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
    +492        """
    +493        Get specific access key.
    +494
    +495        Args:
    +496            key_id: Access key ID
    +497
    +498        Returns:
    +499            Access key details
    +500
    +501        Raises:
    +502            APIError: If key doesn't exist
    +503
    +504        Examples:
    +505            >>> async def doo_something():
    +506            ...     async with AsyncOutlineClient(
    +507            ...         "https://example.com:1234/secret",
    +508            ...         "ab12cd34..."
    +509            ...     ) as client:
    +510            ...         key = await client.get_access_key(1)
    +511            ...         print(f"Port: {key.port}")
    +512            ...         print(f"URL: {key.access_url}")
    +513        """
    +514        response = await self._request("GET", f"access-keys/{key_id}")
    +515        return await self._parse_response(
    +516            response, AccessKey, json_format=self._json_format
    +517        )
     
    @@ -1719,36 +1775,36 @@
    Examples:
    -
    493    async def rename_access_key(self, key_id: int, name: str) -> bool:
    -494        """
    -495        Rename access key.
    -496
    -497        Args:
    -498            key_id: Access key ID
    -499            name: New name
    -500
    -501        Returns:
    -502            True if successful
    -503
    -504        Raises:
    -505            APIError: If key doesn't exist
    -506
    -507        Examples:
    -508            >>> async def doo_something():
    -509            ...     async with AsyncOutlineClient(
    -510            ...         "https://example.com:1234/secret",
    -511            ...         "ab12cd34..."
    -512            ...     ) as client:
    -513            ...         # Rename key
    -514            ...         await client.rename_access_key(1, "Alice")
    -515            ...
    -516            ...         # Verify new name
    -517            ...         key = await client.get_access_key(1)
    -518            ...         assert key.name == "Alice"
    -519        """
    -520        return await self._request(
    -521            "PUT", f"access-keys/{key_id}/name", json={"name": name}
    -522        )
    +            
    519    async def rename_access_key(self, key_id: int, name: str) -> bool:
    +520        """
    +521        Rename access key.
    +522
    +523        Args:
    +524            key_id: Access key ID
    +525            name: New name
    +526
    +527        Returns:
    +528            True if successful
    +529
    +530        Raises:
    +531            APIError: If key doesn't exist
    +532
    +533        Examples:
    +534            >>> async def doo_something():
    +535            ...     async with AsyncOutlineClient(
    +536            ...         "https://example.com:1234/secret",
    +537            ...         "ab12cd34..."
    +538            ...     ) as client:
    +539            ...         # Rename key
    +540            ...         await client.rename_access_key(1, "Alice")
    +541            ...
    +542            ...         # Verify new name
    +543            ...         key = await client.get_access_key(1)
    +544            ...         assert key.name == "Alice"
    +545        """
    +546        return await self._request(
    +547            "PUT", f"access-keys/{key_id}/name", json={"name": name}
    +548        )
     
    @@ -1806,30 +1862,30 @@
    Examples:
    -
    524    async def delete_access_key(self, key_id: int) -> bool:
    -525        """
    -526        Delete access key.
    -527
    -528        Args:
    -529            key_id: Access key ID
    -530
    -531        Returns:
    -532            True if successful
    -533
    -534        Raises:
    -535            APIError: If key doesn't exist
    -536
    -537        Examples:
    -538            >>> async def doo_something():
    -539            ...     async with AsyncOutlineClient(
    -540            ...         "https://example.com:1234/secret",
    -541            ...         "ab12cd34..."
    -542            ...     ) as client:
    -543            ...         if await client.delete_access_key(1):
    -544            ...             print("Key deleted")
    -545
    -546        """
    -547        return await self._request("DELETE", f"access-keys/{key_id}")
    +            
    550    async def delete_access_key(self, key_id: int) -> bool:
    +551        """
    +552        Delete access key.
    +553
    +554        Args:
    +555            key_id: Access key ID
    +556
    +557        Returns:
    +558            True if successful
    +559
    +560        Raises:
    +561            APIError: If key doesn't exist
    +562
    +563        Examples:
    +564            >>> async def doo_something():
    +565            ...     async with AsyncOutlineClient(
    +566            ...         "https://example.com:1234/secret",
    +567            ...         "ab12cd34..."
    +568            ...     ) as client:
    +569            ...         if await client.delete_access_key(1):
    +570            ...             print("Key deleted")
    +571
    +572        """
    +573        return await self._request("DELETE", f"access-keys/{key_id}")
     
    @@ -1882,39 +1938,39 @@
    Examples:
    -
    549    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
    -550        """
    -551        Set data transfer limit for access key.
    -552
    -553        Args:
    -554            key_id: Access key ID
    -555            bytes_limit: Limit in bytes (must be positive)
    -556
    -557        Returns:
    -558            True if successful
    -559
    -560        Raises:
    -561            APIError: If key doesn't exist or limit is invalid
    -562
    -563        Examples:
    -564            >>> async def doo_something():
    -565            ...     async with AsyncOutlineClient(
    -566            ...         "https://example.com:1234/secret",
    -567            ...         "ab12cd34..."
    -568            ...     ) as client:
    -569            ...         # Set 5 GB limit
    -570            ...         limit = 5 * 1024**3  # 5 GB in bytes
    -571            ...         await client.set_access_key_data_limit(1, limit)
    -572            ...
    -573            ...         # Verify limit
    -574            ...         key = await client.get_access_key(1)
    -575            ...         assert key.data_limit and key.data_limit.bytes == limit
    -576        """
    -577        return await self._request(
    -578            "PUT",
    -579            f"access-keys/{key_id}/data-limit",
    -580            json={"limit": {"bytes": bytes_limit}},
    -581        )
    +            
    575    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
    +576        """
    +577        Set data transfer limit for access key.
    +578
    +579        Args:
    +580            key_id: Access key ID
    +581            bytes_limit: Limit in bytes (must be positive)
    +582
    +583        Returns:
    +584            True if successful
    +585
    +586        Raises:
    +587            APIError: If key doesn't exist or limit is invalid
    +588
    +589        Examples:
    +590            >>> async def doo_something():
    +591            ...     async with AsyncOutlineClient(
    +592            ...         "https://example.com:1234/secret",
    +593            ...         "ab12cd34..."
    +594            ...     ) as client:
    +595            ...         # Set 5 GB limit
    +596            ...         limit = 5 * 1024**3  # 5 GB in bytes
    +597            ...         await client.set_access_key_data_limit(1, limit)
    +598            ...
    +599            ...         # Verify limit
    +600            ...         key = await client.get_access_key(1)
    +601            ...         assert key.data_limit and key.data_limit.bytes == limit
    +602        """
    +603        return await self._request(
    +604            "PUT",
    +605            f"access-keys/{key_id}/data-limit",
    +606            json={"limit": {"bytes": bytes_limit}},
    +607        )
     
    @@ -1973,20 +2029,20 @@
    Examples:
    -
    583    async def remove_access_key_data_limit(self, key_id: str) -> bool:
    -584        """
    -585        Remove data transfer limit from access key.
    -586
    -587        Args:
    -588            key_id: Access key ID
    -589
    -590        Returns:
    -591            True if successful
    -592
    -593        Raises:
    -594            APIError: If key doesn't exist
    -595        """
    -596        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
    +            
    609    async def remove_access_key_data_limit(self, key_id: str) -> bool:
    +610        """
    +611        Remove data transfer limit from access key.
    +612
    +613        Args:
    +614            key_id: Access key ID
    +615
    +616        Returns:
    +617            True if successful
    +618
    +619        Raises:
    +620            APIError: If key doesn't exist
    +621        """
    +622        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
     
    @@ -2025,8 +2081,8 @@
    Raises:
    -
    25class OutlineError(Exception):
    -26    """Base exception for Outline client errors."""
    +            
    47class OutlineError(Exception):
    +48    """Base exception for Outline client errors."""
     
    @@ -2046,8 +2102,12 @@
    Raises:
    -
    29class APIError(OutlineError):
    -30    """Raised when API requests fail."""
    +            
    51class APIError(OutlineError):
    +52    """Raised when API requests fail."""
    +53
    +54    def __init__(self, message: str, status_code: Optional[int] = None) -> None:
    +55        super().__init__(message)
    +56        self.status_code = status_code
     
    @@ -2055,6 +2115,36 @@
    Raises:
    +
    + +
    + + APIError(message: str, status_code: Optional[int] = None) + + + +
    + +
    54    def __init__(self, message: str, status_code: Optional[int] = None) -> None:
    +55        super().__init__(message)
    +56        self.status_code = status_code
    +
    + + + + +
    +
    +
    + status_code + + +
    + + + + +
    @@ -2067,16 +2157,16 @@
    Raises:
    -
    28class AccessKey(BaseModel):
    -29    """Access key details."""
    -30
    -31    id: int
    -32    name: Optional[str] = None
    -33    password: str
    -34    port: int = Field(gt=0, lt=65536)
    -35    method: str
    -36    access_url: str = Field(alias="accessUrl")
    -37    data_limit: Optional[DataLimit] = Field(None, alias="dataLimit")
    +            
    41class AccessKey(BaseModel):
    +42    """Access key details."""
    +43
    +44    id: int
    +45    name: Optional[str] = None
    +46    password: str
    +47    port: int = Field(gt=0, lt=65536)
    +48    method: str
    +49    access_url: str = Field(alias="accessUrl")
    +50    data_limit: Optional[DataLimit] = Field(None, alias="dataLimit")
     
    @@ -2187,17 +2277,17 @@
    Raises:
    -
    105class AccessKeyCreateRequest(BaseModel):
    -106    """
    -107    Request parameters for creating an access key.
    -108    Per OpenAPI: /access-keys POST request body
    -109    """
    -110
    -111    name: Optional[str] = None
    -112    method: Optional[str] = None
    -113    password: Optional[str] = None
    -114    port: Optional[int] = Field(None, gt=0, lt=65536)
    -115    limit: Optional[DataLimit] = None
    +            
    118class AccessKeyCreateRequest(BaseModel):
    +119    """
    +120    Request parameters for creating an access key.
    +121    Per OpenAPI: /access-keys POST request body
    +122    """
    +123
    +124    name: Optional[str] = None
    +125    method: Optional[str] = None
    +126    password: Optional[str] = None
    +127    port: Optional[int] = Field(None, gt=0, lt=65536)
    +128    limit: Optional[DataLimit] = None
     
    @@ -2287,10 +2377,10 @@
    Raises:
    -
    40class AccessKeyList(BaseModel):
    -41    """List of access keys."""
    -42
    -43    access_keys: list[AccessKey] = Field(alias="accessKeys")
    +            
    53class AccessKeyList(BaseModel):
    +54    """List of access keys."""
    +55
    +56    access_keys: list[AccessKey] = Field(alias="accessKeys")
     
    @@ -2335,16 +2425,16 @@
    Raises:
    -
    16class DataLimit(BaseModel):
    -17    """Data transfer limit configuration."""
    -18
    -19    bytes: int = Field(gt=0)
    -20
    -21    @field_validator("bytes")
    -22    def validate_bytes(cls, v: int) -> int:
    -23        if v < 0:
    -24            raise ValueError("bytes must be positive")
    -25        return v
    +            
    29class DataLimit(BaseModel):
    +30    """Data transfer limit configuration."""
    +31
    +32    bytes: int = Field(gt=0)
    +33
    +34    @field_validator("bytes")
    +35    def validate_bytes(cls, v: int) -> int:
    +36        if v < 0:
    +37            raise ValueError("bytes must be positive")
    +38        return v
     
    @@ -2375,11 +2465,11 @@
    Raises:
    -
    21    @field_validator("bytes")
    -22    def validate_bytes(cls, v: int) -> int:
    -23        if v < 0:
    -24            raise ValueError("bytes must be positive")
    -25        return v
    +            
    34    @field_validator("bytes")
    +35    def validate_bytes(cls, v: int) -> int:
    +36        if v < 0:
    +37            raise ValueError("bytes must be positive")
    +38        return v
     
    @@ -2412,14 +2502,14 @@
    Raises:
    -
    124class ErrorResponse(BaseModel):
    -125    """
    -126    Error response structure
    -127    Per OpenAPI: 404 and 400 responses
    -128    """
    -129
    -130    code: str
    -131    message: str
    +            
    137class ErrorResponse(BaseModel):
    +138    """
    +139    Error response structure
    +140    Per OpenAPI: 404 and 400 responses
    +141    """
    +142
    +143    code: str
    +144    message: str
     
    @@ -2476,14 +2566,14 @@
    Raises:
    -
    79class ExperimentalMetrics(BaseModel):
    -80    """
    -81    Experimental metrics data structure
    -82    Per OpenAPI: /experimental/server/metrics endpoint
    -83    """
    -84
    -85    server: list[ServerMetric]
    -86    access_keys: list[AccessKeyMetric] = Field(alias="accessKeys")
    +            
    92class ExperimentalMetrics(BaseModel):
    +93    """
    +94    Experimental metrics data structure
    +95    Per OpenAPI: /experimental/server/metrics endpoint
    +96    """
    +97
    +98    server: list[ServerMetric]
    +99    access_keys: list[AccessKeyMetric] = Field(alias="accessKeys")
     
    @@ -2540,12 +2630,12 @@
    Raises:
    -
     8class MetricsPeriod(str, Enum):
    - 9    """Time periods for metrics collection."""
    -10
    -11    DAILY = "daily"
    -12    WEEKLY = "weekly"
    -13    MONTHLY = "monthly"
    +            
    21class MetricsPeriod(str, Enum):
    +22    """Time periods for metrics collection."""
    +23
    +24    DAILY = "daily"
    +25    WEEKLY = "weekly"
    +26    MONTHLY = "monthly"
     
    @@ -2601,10 +2691,10 @@
    Raises:
    -
    118class MetricsStatusResponse(BaseModel):
    -119    """Response for /metrics/enabled endpoint"""
    -120
    -121    metrics_enabled: bool = Field(alias="metricsEnabled")
    +            
    131class MetricsStatusResponse(BaseModel):
    +132    """Response for /metrics/enabled endpoint"""
    +133
    +134    metrics_enabled: bool = Field(alias="metricsEnabled")
     
    @@ -2649,20 +2739,20 @@
    Raises:
    -
     89class Server(BaseModel):
    - 90    """
    - 91    Server information.
    - 92    Per OpenAPI: /server endpoint schema
    - 93    """
    - 94
    - 95    name: str
    - 96    server_id: str = Field(alias="serverId")
    - 97    metrics_enabled: bool = Field(alias="metricsEnabled")
    - 98    created_timestamp_ms: int = Field(alias="createdTimestampMs")
    - 99    version: str
    -100    port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536)
    -101    hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys")
    -102    access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit")
    +            
    102class Server(BaseModel):
    +103    """
    +104    Server information.
    +105    Per OpenAPI: /server endpoint schema
    +106    """
    +107
    +108    name: str
    +109    server_id: str = Field(alias="serverId")
    +110    metrics_enabled: bool = Field(alias="metricsEnabled")
    +111    created_timestamp_ms: int = Field(alias="createdTimestampMs")
    +112    version: str
    +113    port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536)
    +114    hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys")
    +115    access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit")
     
    @@ -2785,15 +2875,15 @@
    Raises:
    -
    46class ServerMetrics(BaseModel):
    -47    """
    -48    Server metrics data for data transferred per access key
    -49    Per OpenAPI: /metrics/transfer endpoint
    -50    """
    -51
    -52    bytes_transferred_by_user_id: dict[str, int] = Field(
    -53        alias="bytesTransferredByUserId"
    -54    )
    +            
    59class ServerMetrics(BaseModel):
    +60    """
    +61    Server metrics data for data transferred per access key
    +62    Per OpenAPI: /metrics/transfer endpoint
    +63    """
    +64
    +65    bytes_transferred_by_user_id: dict[str, int] = Field(
    +66        alias="bytesTransferredByUserId"
    +67    )
     
    diff --git a/docs/search.js b/docs/search.js index 6fbb620..3e0bc1d 100644 --- a/docs/search.js +++ b/docs/search.js @@ -1,6 +1,6 @@ window.pdocSearch = (function(){ /** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o

    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

    Asynchronous client for the Outline VPN Server API.

    \n\n
    Arguments:
    \n\n
      \n
    • api_url: Base URL for the Outline server API
    • \n
    • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    • \n
    • json_format: Return raw JSON instead of Pydantic models
    • \n
    • timeout: Request timeout in seconds
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server_info = await client.get_server_info()\n
    \n
    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\tjson_format: bool = True,\ttimeout: float = 30.0)"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_info", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_info", "kind": "function", "doc": "

    Get server information.

    \n\n
    Returns:
    \n\n
    \n

    Server information including name, ID, and configuration.

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server = await client.get_server_info()\n...         print(f"Server {server.name} running version {server.version}")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.Server]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_server", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_server", "kind": "function", "doc": "

    Rename the server.

    \n\n
    Arguments:
    \n\n
      \n
    • name: New server name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...     success = await client.rename_server("My VPN Server")\n...     if success:\n...         print("Server renamed successfully")\n
    \n
    \n
    \n", "signature": "(self, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_hostname", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_hostname", "kind": "function", "doc": "

    Set server hostname for access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • hostname: New hostname or IP address
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If hostname is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_hostname("vpn.example.com")\n...         # Or use IP address\n...         await client.set_hostname("203.0.113.1")\n
    \n
    \n
    \n", "signature": "(self, hostname: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_default_port", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_default_port", "kind": "function", "doc": "

    Set default port for new access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • port: Port number (1025-65535)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If port is invalid or in use
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_default_port(8388)\n
    \n
    \n
    \n", "signature": "(self, port: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_metrics_status", "kind": "function", "doc": "

    Get whether metrics collection is enabled.

    \n\n
    Returns:
    \n\n
    \n

    Current metrics collection status

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.get_metrics_status():\n...             print("Metrics collection is enabled")\n
    \n
    \n
    \n", "signature": "(self) -> dict[str, typing.Any] | pydantic.main.BaseModel:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_metrics_status", "kind": "function", "doc": "

    Enable or disable metrics collection.

    \n\n
    Arguments:
    \n\n
      \n
    • enabled: Whether to enable metrics
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Enable metrics\n...         await client.set_metrics_status(True)\n...         # Check new status\n...         is_enabled = await client.get_metrics_status()\n
    \n
    \n
    \n", "signature": "(self, enabled: bool) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_transfer_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_transfer_metrics", "kind": "function", "doc": "

    Get transfer metrics for specified period.

    \n\n
    Arguments:
    \n\n
      \n
    • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Transfer metrics data for each access key

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Get monthly metrics\n...         metrics = await client.get_transfer_metrics()\n...         # Or get daily metrics\n...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)\n...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():\n...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")\n
    \n
    \n
    \n", "signature": "(\tself,\tperiod: pyoutlineapi.models.MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], pyoutlineapi.models.ServerMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key", "kind": "function", "doc": "

    Create a new access key.

    \n\n
    Arguments:
    \n\n
      \n
    • name: Optional key name
    • \n
    • password: Optional password
    • \n
    • port: Optional port number (1-65535)
    • \n
    • method: Optional encryption method
    • \n
    • limit: Optional data transfer limit
    • \n
    \n\n
    Returns:
    \n\n
    \n

    New access key details

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Create basic key\n...         key = await client.create_access_key(name="User 1")\n...\n...         # Create key with data limit\n...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB\n...         key = await client.create_access_key(\n...             name="Limited User",\n...             port=8388,\n...             limit=_limit\n...         )\n...         print(f"Created key: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_keys", "kind": "function", "doc": "

    Get all access keys.

    \n\n
    Returns:
    \n\n
    \n

    List of all access keys

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         keys = await client.get_access_keys()\n...         for key in keys.access_keys:\n...             print(f"Key {key.id}: {key.name or 'unnamed'}")\n...             if key.data_limit:\n...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.AccessKeyList]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_key", "kind": "function", "doc": "

    Get specific access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Access key details

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.get_access_key(1)\n...         print(f"Port: {key.port}")\n...         print(f"URL: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\tkey_id: int) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_access_key", "kind": "function", "doc": "

    Rename access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • name: New name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Rename key\n...         await client.rename_access_key(1, "Alice")\n...\n...         # Verify new name\n...         key = await client.get_access_key(1)\n...         assert key.name == "Alice"\n
    \n
    \n
    \n", "signature": "(self, key_id: int, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.delete_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.delete_access_key", "kind": "function", "doc": "

    Delete access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.delete_access_key(1):\n...             print("Key deleted")\n
    \n
    \n
    \n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_access_key_data_limit", "kind": "function", "doc": "

    Set data transfer limit for access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • bytes_limit: Limit in bytes (must be positive)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist or limit is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 5 GB limit\n...         limit = 5 * 1024**3  # 5 GB in bytes\n...         await client.set_access_key_data_limit(1, limit)\n...\n...         # Verify limit\n...         key = await client.get_access_key(1)\n...         assert key.data_limit and key.data_limit.bytes == limit\n
    \n
    \n
    \n", "signature": "(self, key_id: int, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_access_key_data_limit", "kind": "function", "doc": "

    Remove data transfer limit from access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n", "signature": "(self, key_id: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

    Base exception for Outline client errors.

    \n", "bases": "builtins.Exception"}, {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

    Raised when API requests fail.

    \n", "bases": "pyoutlineapi.client.OutlineError"}, {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

    Access key details.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKey.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKey.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

    Request parameters for creating an access key.\nPer OpenAPI: /access-keys POST request body

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[int]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

    List of access keys.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKey]"}, {"fullname": "pyoutlineapi.AccessKeyList.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

    Data transfer limit configuration.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.DataLimit.validate_bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.validate_bytes", "kind": "function", "doc": "

    \n", "signature": "(cls, v: int) -> int:", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

    Error response structure\nPer OpenAPI: 404 and 400 responses

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.model_config", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

    Experimental metrics data structure\nPer OpenAPI: /experimental/server/metrics endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.ServerMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsPeriod", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod", "kind": "class", "doc": "

    Time periods for metrics collection.

    \n", "bases": "builtins.str, enum.Enum"}, {"fullname": "pyoutlineapi.MetricsPeriod.DAILY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.DAILY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.DAILY: 'daily'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.WEEKLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.WEEKLY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.WEEKLY: 'weekly'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.MONTHLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.MONTHLY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.MONTHLY: 'monthly'>"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

    Response for /metrics/enabled endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

    Server information.\nPer OpenAPI: /server endpoint schema

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.Server.model_config", "modulename": "pyoutlineapi", "qualname": "Server.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

    Server metrics data for data transferred per access key\nPer OpenAPI: /metrics/transfer endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, int]"}, {"fullname": "pyoutlineapi.ServerMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}]; + /** pdoc search index */const docs = [{"fullname": "pyoutlineapi", "modulename": "pyoutlineapi", "kind": "module", "doc": "

    PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    \n\n

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru\nAll rights reserved.

    \n\n

    This software is licensed under the MIT License.

    \n\n
    You can find the full license text at:
    \n\n
    \n

    https://opensource.org/licenses/MIT

    \n
    \n\n
    Source code repository:
    \n\n
    \n

    https://github.com/orenlab/pyoutlineapi

    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

    Asynchronous client for the Outline VPN Server API.

    \n\n
    Arguments:
    \n\n
      \n
    • api_url: Base URL for the Outline server API
    • \n
    • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    • \n
    • json_format: Return raw JSON instead of Pydantic models
    • \n
    • timeout: Request timeout in seconds
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server_info = await client.get_server_info()\n
    \n
    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\tjson_format: bool = True,\ttimeout: float = 30.0)"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_info", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_info", "kind": "function", "doc": "

    Get server information.

    \n\n
    Returns:
    \n\n
    \n

    Server information including name, ID, and configuration.

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server = await client.get_server_info()\n...         print(f"Server {server.name} running version {server.version}")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.Server]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_server", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_server", "kind": "function", "doc": "

    Rename the server.

    \n\n
    Arguments:
    \n\n
      \n
    • name: New server name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...     success = await client.rename_server("My VPN Server")\n...     if success:\n...         print("Server renamed successfully")\n
    \n
    \n
    \n", "signature": "(self, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_hostname", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_hostname", "kind": "function", "doc": "

    Set server hostname for access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • hostname: New hostname or IP address
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If hostname is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_hostname("vpn.example.com")\n...         # Or use IP address\n...         await client.set_hostname("203.0.113.1")\n
    \n
    \n
    \n", "signature": "(self, hostname: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_default_port", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_default_port", "kind": "function", "doc": "

    Set default port for new access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • port: Port number (1025-65535)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If port is invalid or in use
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_default_port(8388)\n
    \n
    \n
    \n", "signature": "(self, port: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_metrics_status", "kind": "function", "doc": "

    Get whether metrics collection is enabled.

    \n\n
    Returns:
    \n\n
    \n

    Current metrics collection status

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.get_metrics_status():\n...             print("Metrics collection is enabled")\n
    \n
    \n
    \n", "signature": "(self) -> dict[str, typing.Any] | pydantic.main.BaseModel:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_metrics_status", "kind": "function", "doc": "

    Enable or disable metrics collection.

    \n\n
    Arguments:
    \n\n
      \n
    • enabled: Whether to enable metrics
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Enable metrics\n...         await client.set_metrics_status(True)\n...         # Check new status\n...         is_enabled = await client.get_metrics_status()\n
    \n
    \n
    \n", "signature": "(self, enabled: bool) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_transfer_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_transfer_metrics", "kind": "function", "doc": "

    Get transfer metrics for specified period.

    \n\n
    Arguments:
    \n\n
      \n
    • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Transfer metrics data for each access key

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Get monthly metrics\n...         metrics = await client.get_transfer_metrics()\n...         # Or get daily metrics\n...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)\n...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():\n...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")\n
    \n
    \n
    \n", "signature": "(\tself,\tperiod: pyoutlineapi.models.MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], pyoutlineapi.models.ServerMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key", "kind": "function", "doc": "

    Create a new access key.

    \n\n
    Arguments:
    \n\n
      \n
    • name: Optional key name
    • \n
    • password: Optional password
    • \n
    • port: Optional port number (1-65535)
    • \n
    • method: Optional encryption method
    • \n
    • limit: Optional data transfer limit
    • \n
    \n\n
    Returns:
    \n\n
    \n

    New access key details

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Create basic key\n...         key = await client.create_access_key(name="User 1")\n...\n...         # Create key with data limit\n...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB\n...         key = await client.create_access_key(\n...             name="Limited User",\n...             port=8388,\n...             limit=_limit\n...         )\n...         print(f"Created key: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_keys", "kind": "function", "doc": "

    Get all access keys.

    \n\n
    Returns:
    \n\n
    \n

    List of all access keys

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         keys = await client.get_access_keys()\n...         for key in keys.access_keys:\n...             print(f"Key {key.id}: {key.name or 'unnamed'}")\n...             if key.data_limit:\n...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.AccessKeyList]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_key", "kind": "function", "doc": "

    Get specific access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Access key details

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.get_access_key(1)\n...         print(f"Port: {key.port}")\n...         print(f"URL: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\tkey_id: int) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_access_key", "kind": "function", "doc": "

    Rename access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • name: New name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Rename key\n...         await client.rename_access_key(1, "Alice")\n...\n...         # Verify new name\n...         key = await client.get_access_key(1)\n...         assert key.name == "Alice"\n
    \n
    \n
    \n", "signature": "(self, key_id: int, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.delete_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.delete_access_key", "kind": "function", "doc": "

    Delete access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.delete_access_key(1):\n...             print("Key deleted")\n
    \n
    \n
    \n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_access_key_data_limit", "kind": "function", "doc": "

    Set data transfer limit for access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • bytes_limit: Limit in bytes (must be positive)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist or limit is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 5 GB limit\n...         limit = 5 * 1024**3  # 5 GB in bytes\n...         await client.set_access_key_data_limit(1, limit)\n...\n...         # Verify limit\n...         key = await client.get_access_key(1)\n...         assert key.data_limit and key.data_limit.bytes == limit\n
    \n
    \n
    \n", "signature": "(self, key_id: int, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_access_key_data_limit", "kind": "function", "doc": "

    Remove data transfer limit from access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n", "signature": "(self, key_id: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

    Base exception for Outline client errors.

    \n", "bases": "builtins.Exception"}, {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

    Raised when API requests fail.

    \n", "bases": "pyoutlineapi.client.OutlineError"}, {"fullname": "pyoutlineapi.APIError.__init__", "modulename": "pyoutlineapi", "qualname": "APIError.__init__", "kind": "function", "doc": "

    \n", "signature": "(message: str, status_code: Optional[int] = None)"}, {"fullname": "pyoutlineapi.APIError.status_code", "modulename": "pyoutlineapi", "qualname": "APIError.status_code", "kind": "variable", "doc": "

    \n"}, {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

    Access key details.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKey.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKey.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

    Request parameters for creating an access key.\nPer OpenAPI: /access-keys POST request body

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[int]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

    List of access keys.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKey]"}, {"fullname": "pyoutlineapi.AccessKeyList.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

    Data transfer limit configuration.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.DataLimit.validate_bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.validate_bytes", "kind": "function", "doc": "

    \n", "signature": "(cls, v: int) -> int:", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

    Error response structure\nPer OpenAPI: 404 and 400 responses

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.model_config", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

    Experimental metrics data structure\nPer OpenAPI: /experimental/server/metrics endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.ServerMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsPeriod", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod", "kind": "class", "doc": "

    Time periods for metrics collection.

    \n", "bases": "builtins.str, enum.Enum"}, {"fullname": "pyoutlineapi.MetricsPeriod.DAILY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.DAILY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.DAILY: 'daily'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.WEEKLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.WEEKLY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.WEEKLY: 'weekly'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.MONTHLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.MONTHLY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.MONTHLY: 'monthly'>"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

    Response for /metrics/enabled endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

    Server information.\nPer OpenAPI: /server endpoint schema

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.Server.model_config", "modulename": "pyoutlineapi", "qualname": "Server.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

    Server metrics data for data transferred per access key\nPer OpenAPI: /metrics/transfer endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, int]"}, {"fullname": "pyoutlineapi.ServerMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}]; // mirrored in build-search-index.js (part 1) // Also split on html tags. this is a cheap heuristic, but good enough. diff --git a/pyoutlineapi/__init__.py b/pyoutlineapi/__init__.py index 6009ca9..8ef0e72 100644 --- a/pyoutlineapi/__init__.py +++ b/pyoutlineapi/__init__.py @@ -1,20 +1,44 @@ +""" +PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" +import sys +from typing import TYPE_CHECKING + +if sys.version_info < (3, 10): + raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher") + from .client import AsyncOutlineClient, OutlineError, APIError -from .models import ( - AccessKey, - AccessKeyCreateRequest, - AccessKeyList, - DataLimit, - ErrorResponse, - ExperimentalMetrics, - MetricsPeriod, - MetricsStatusResponse, - Server, - ServerMetrics, -) -__version__ = "0.2.0" +if TYPE_CHECKING: + from .models import ( + AccessKey, + AccessKeyCreateRequest, + AccessKeyList, + DataLimit, + ErrorResponse, + ExperimentalMetrics, + MetricsPeriod, + MetricsStatusResponse, + Server, + ServerMetrics, + ) + +__version__: str = "0.2.0" +__author__ = "Denis Rozhnovskiy" +__email__ = "pytelemonbot@mail.ru" +__license__ = "MIT" -__all__ = [ +PUBLIC_API = [ "AsyncOutlineClient", "OutlineError", "APIError", @@ -29,3 +53,19 @@ "Server", "ServerMetrics", ] + +__all__ = PUBLIC_API + +# Actual imports for runtime +from .models import ( + AccessKey, + AccessKeyCreateRequest, + AccessKeyList, + DataLimit, + ErrorResponse, + ExperimentalMetrics, + MetricsPeriod, + MetricsStatusResponse, + Server, + ServerMetrics, +) diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index 726615d..2d5c618 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -1,7 +1,21 @@ +""" +PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" from __future__ import annotations import binascii -from typing import Any, Literal, TypeAlias, Union, overload, Optional +from functools import wraps +from typing import Any, Literal, TypeAlias, Union, overload, Optional, ParamSpec, TypeVar, Callable from urllib.parse import urlparse import aiohttp @@ -20,6 +34,14 @@ ServerMetrics, ) +# Type variables for decorator +P = ParamSpec('P') +T = TypeVar('T') + +# Type aliases +JsonDict: TypeAlias = dict[str, Any] +ResponseType = Union[JsonDict, BaseModel] + class OutlineError(Exception): """Base exception for Outline client errors.""" @@ -28,9 +50,21 @@ class OutlineError(Exception): class APIError(OutlineError): """Raised when API requests fail.""" + def __init__(self, message: str, status_code: Optional[int] = None) -> None: + super().__init__(message) + self.status_code = status_code -# Type aliases -JsonDict: TypeAlias = dict[str, Any] + +def ensure_context(func: Callable[P, T]) -> Callable[P, T]: + """Decorator to ensure client session is initialized.""" + + @wraps(func) + async def wrapper(self: AsyncOutlineClient, *args: P.args, **kwargs: P.kwargs) -> T: + if not self._session or self._session.closed: + raise RuntimeError("Client session is not initialized or already closed.") + return await func(self, *args, **kwargs) + + return wrapper class AsyncOutlineClient: @@ -53,27 +87,27 @@ class AsyncOutlineClient: """ def __init__( - self, - api_url: str, - cert_sha256: str, - *, - json_format: bool = True, - timeout: float = 30.0, + self, + api_url: str, + cert_sha256: str, + *, + json_format: bool = True, + timeout: float = 30.0, ) -> None: self._api_url = api_url.rstrip("/") self._cert_sha256 = cert_sha256 self._json_format = json_format self._timeout = aiohttp.ClientTimeout(total=timeout) - self._ssl_context = None + self._ssl_context: Optional[Fingerprint] = None self._session: Optional[aiohttp.ClientSession] = None - self._in_context = False async def __aenter__(self) -> AsyncOutlineClient: """Set up client session for context manager.""" self._session = aiohttp.ClientSession( - timeout=self._timeout, raise_for_status=True + timeout=self._timeout, + raise_for_status=False, + connector=aiohttp.TCPConnector(ssl=self._get_ssl_context()) ) - self._in_context = True return self async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: @@ -81,37 +115,38 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if self._session: await self._session.close() self._session = None - self._in_context = False - - def _ensure_context(self): - """Ensure the session context is valid.""" - if not self._session or self._session.closed: - raise RuntimeError("Client session is not initialized or already closed.") @overload async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: Literal[True], - ) -> JsonDict: ... + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[True], + ) -> JsonDict: + ... @overload async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: Literal[False], - ) -> BaseModel: ... + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[False], + ) -> BaseModel: + ... @overload async def _parse_response( - self, response: ClientResponse, model: type[BaseModel], json_format: bool - ) -> Union[JsonDict, BaseModel]: ... + self, response: ClientResponse, model: type[BaseModel], json_format: bool + ) -> Union[JsonDict, BaseModel]: + ... + @ensure_context async def _parse_response( - self, response: ClientResponse, model: type[BaseModel], json_format: bool = True - ) -> Union[JsonDict, BaseModel]: + self, + response: ClientResponse, + model: type[BaseModel], + json_format: bool = True + ) -> ResponseType: """ Parse and validate API response data. @@ -126,17 +161,14 @@ async def _parse_response( Raises: ValueError: If response validation fails """ - self._ensure_context() - try: data = await response.json() - except aiohttp.ContentTypeError: - raise ValueError("Invalid response format") from None - try: validated = model.model_validate(data) return validated.model_dump() if json_format else validated + except aiohttp.ContentTypeError as e: + raise ValueError("Invalid response format") from e except Exception as e: - raise ValueError(f"Value error: {e}") from e + raise ValueError(f"Validation error: {e}") from e @staticmethod async def _handle_error_response(response: ClientResponse) -> None: @@ -144,57 +176,52 @@ async def _handle_error_response(response: ClientResponse) -> None: try: error_data = await response.json() error = ErrorResponse.model_validate(error_data) - raise APIError(f"{error.code}: {error.message}") + raise APIError(f"{error.code}: {error.message}", response.status) except ValueError: - raise APIError(f"HTTP {response.status}: {response.reason}") + raise APIError(f"HTTP {response.status}: {response.reason}", response.status) + @ensure_context async def _request( - self, - method: str, - endpoint: str, - *, - json: Any = None, - params: Optional[dict[str, Any]] = None, + self, + method: str, + endpoint: str, + *, + json: Any = None, + params: Optional[dict[str, Any]] = None, ) -> Any: """Make an API request.""" - self._ensure_context() - url = self._build_url(endpoint) - ssl_context = self._get_ssl_context() async with self._session.request( - method, - url, - json=json, - params=params, - ssl=ssl_context, - raise_for_status=False, - timeout=self._timeout, + method, + url, + json=json, + params=params, + raise_for_status=False, ) as response: if response.status >= 400: await self._handle_error_response(response) if response.status == 204: - return True # No content response + return True try: await response.json() return response except aiohttp.ContentTypeError: - return await response.text() # Fallback for non-JSON responses + return await response.text() except Exception as e: - raise APIError(f"Failed to parse response from {url}: {e}") from e + raise APIError(f"Failed to parse response: {e}", response.status) def _build_url(self, endpoint: str) -> str: """Build and validate the full URL for the API request.""" if not isinstance(endpoint, str): raise ValueError("Endpoint must be a string") - endpoint = endpoint.lstrip("/") - url = f"{self._api_url}/{endpoint}" - + url = f"{self._api_url}/{endpoint.lstrip('/')}" parsed_url = urlparse(url) - if not parsed_url.scheme or not parsed_url.netloc: + + if not all([parsed_url.scheme, parsed_url.netloc]): raise ValueError(f"Invalid URL: {url}") return url @@ -205,12 +232,11 @@ def _get_ssl_context(self) -> Optional[Fingerprint]: return None try: - fingerprint = binascii.unhexlify(self._cert_sha256) - return Fingerprint(fingerprint) + return Fingerprint(binascii.unhexlify(self._cert_sha256)) except binascii.Error as e: raise ValueError(f"Invalid certificate SHA256: {self._cert_sha256}") from e except Exception as e: - raise OutlineError("Error while creating SSL context") from e + raise OutlineError("Failed to create SSL context") from e async def get_server_info(self) -> Union[JsonDict, Server]: """ @@ -356,7 +382,7 @@ async def set_metrics_status(self, enabled: bool) -> bool: ) async def get_transfer_metrics( - self, period: MetricsPeriod = MetricsPeriod.MONTHLY + self, period: MetricsPeriod = MetricsPeriod.MONTHLY ) -> Union[JsonDict, ServerMetrics]: """ Get transfer metrics for specified period. @@ -388,13 +414,13 @@ async def get_transfer_metrics( ) async def create_access_key( - self, - *, - name: Optional[str] = None, - password: Optional[str] = None, - port: Optional[int] = None, - method: Optional[str] = None, - limit: Optional[DataLimit] = None, + self, + *, + name: Optional[str] = None, + password: Optional[str] = None, + port: Optional[int] = None, + method: Optional[str] = None, + limit: Optional[DataLimit] = None, ) -> Union[JsonDict, AccessKey]: """ Create a new access key. diff --git a/pyoutlineapi/models.py b/pyoutlineapi/models.py index 5b50da0..d3af8c6 100644 --- a/pyoutlineapi/models.py +++ b/pyoutlineapi/models.py @@ -1,3 +1,16 @@ +""" +PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" from enum import Enum from typing import Optional diff --git a/pyproject.toml b/pyproject.toml index 8369d3a..18bd6bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,9 +43,10 @@ ruff = "^0.3.0" pdoc = "^15.0.1" [tool.pytest.ini_options] -addopts = "--cov=pyoutlineapi --cov-report=term-missing --cov-report=xml --cov-report=html" -testpaths = ["tests"] asyncio_mode = "auto" +testpaths = ["tests"] +python_files = ["test_*.py"] +addopts = "-v --cov=pyoutlineapi --cov-report=html --cov-report=xml" [tool.black] line-length = 88 @@ -66,7 +67,6 @@ lint.select = ["E", "F", "B", "I"] [tool.poetry.urls] homepage = "https://github.com/orenlab/pyoutlineapi" -repository = "https://github.com/orenlab/pyoutlineapi" documentation = "https://github.com/orenlab/pyoutlineapi/blob/main/README.md" changelog = "https://github.com/orenlab/pyoutlineapi/blob/main/CHANGELOG.md" From 9210b215a59c67d3df80e05c5c648a4c8eeedb34 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 13:35:51 +0500 Subject: [PATCH 13/24] chore(ci): fix docs.yml GitHub Action script --- .github/workflows/docs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b59f858..0be162a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,9 +25,10 @@ jobs: # ADJUST THIS: install all dependencies (including pdoc) - run: pip install -e . + - run: pip install pdoc # ADJUST THIS: build your documentation into docs/. # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. - - run: python docs/make.py + - run: pdoc -d google pyoutlineapi --output-dir docs - uses: actions/upload-pages-artifact@v3 with: From a1c4b4f4122091939d867b6f657b97f89f75508d Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 13:40:28 +0500 Subject: [PATCH 14/24] chore(ci): fix docs.yml GitHub Action script --- .github/workflows/docs.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0be162a..c5523ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,10 +4,7 @@ name: website on: push: branches: - - development - # Alternative: only build for tags. - # tags: - # - '*' + - main # security: restrict permissions for CI jobs. permissions: @@ -22,12 +19,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.13' - - # ADJUST THIS: install all dependencies (including pdoc) - run: pip install -e . - run: pip install pdoc - # ADJUST THIS: build your documentation into docs/. - # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. - run: pdoc -d google pyoutlineapi --output-dir docs - uses: actions/upload-pages-artifact@v3 From d56f40acf05ffaceb4e437934374d7f46b693d84 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 17:45:50 +0500 Subject: [PATCH 15/24] test: added part off tests --- .github/workflows/python_tests.yml | 93 +++++++++-- poetry.lock | 4 +- pyoutlineapi/__init__.py | 2 +- pyoutlineapi/client.py | 4 + pyproject.toml | 2 +- tests/test_outline_api.py | 256 +++++++++++++++++++++++++++++ 6 files changed, 339 insertions(+), 22 deletions(-) create mode 100644 tests/test_outline_api.py diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index 1b3d5b1..b141a7f 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -5,38 +5,95 @@ on: branches: [ "main", "development" ] pull_request: branches: [ "main" ] + schedule: + - cron: '0 0 * * 0' # Weekly security scan permissions: contents: read + pull-requests: write + security-events: write # Required for security findings jobs: - build: + test: + name: Run Tests runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + steps: - uses: actions/checkout@v4 - - name: Set up Python 3.12 - uses: actions/setup-python@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Poetry - run: | - curl -sSL https://install.python-poetry.org | python3 - - echo "export PATH=$HOME/.local/bin:$PATH" >> $GITHUB_ENV + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction + + - name: Run tests run: | - poetry install - - name: Install flake8 + poetry run pytest --cov=./ --cov-report=xml + + - name: Run linting run: | - poetry run pip install flake8 - - name: Lint with flake8 + poetry run flake8 . --count --statistics \ + --max-line-length=88 \ + --extend-ignore=E203 \ + --max-complexity=10 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + fail_ci_if_error: true + + security: + name: Security Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: 'pip' + + - name: Install security tools run: | - poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest + python -m pip install safety + + - name: Run safety check run: | - poetry run pytest --cov - - name: Upload results to Codecov - uses: codecov/codecov-action@v4 + safety check + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 with: - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + category: "/language:python" \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 0b19784..8abdbf8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1292,5 +1292,5 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" -python-versions = ">=3.10,<4.0" -content-hash = "0cf9f74293d58c162e15afc8a3627c83fa8a8de172466a15277eed65dfe17a55" +python-versions = ">=3.9,<4.0" +content-hash = "9ce916bd51e0aa973c0eccd7ed78320588b95542802e99a2d8bb3e1f7b7a547b" diff --git a/pyoutlineapi/__init__.py b/pyoutlineapi/__init__.py index 8ef0e72..271077c 100644 --- a/pyoutlineapi/__init__.py +++ b/pyoutlineapi/__init__.py @@ -14,7 +14,7 @@ import sys from typing import TYPE_CHECKING -if sys.version_info < (3, 10): +if sys.version_info < (3, 9): raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher") from .client import AsyncOutlineClient, OutlineError, APIError diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index 2d5c618..ee19e51 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -619,3 +619,7 @@ async def remove_access_key_data_limit(self, key_id: str) -> bool: APIError: If key doesn't exist """ return await self._request("DELETE", f"access-keys/{key_id}/data-limit") + + @property + def session(self): + return self._session diff --git a/pyproject.toml b/pyproject.toml index 18bd6bf..4854ea0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">=3.10,<4.0" +python = ">=3.9,<4.0" pydantic = "^2.9.2" aiohttp = "^3.11.11" diff --git a/tests/test_outline_api.py b/tests/test_outline_api.py new file mode 100644 index 0000000..7ec9bd6 --- /dev/null +++ b/tests/test_outline_api.py @@ -0,0 +1,256 @@ +from datetime import datetime, timezone +from typing import AsyncGenerator, Dict +from unittest.mock import MagicMock + +import pytest +from aiohttp import ClientSession + +from pyoutlineapi import AsyncOutlineClient, APIError +from pyoutlineapi.models import ( + DataLimit +) + +# Constants for testing +TEST_API_URL = "https://example.com:1234/secret" +TEST_CERT_SHA256 = "ab12cd34ef56gh78ij90kl12mn34op56qr78st90uvwxyzabcdef123456" +TEST_SERVER_ID = "server-id-123" +TEST_SERVER_NAME = "Test Server" + + +class MockResponse: + """Mock response that can be used as an async context manager.""" + + def __init__(self, status: int = 200, data: dict = None): + self.status = status + self._data = data or {} + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + async def json(self): + return self._data + + +@pytest.fixture +def server_info() -> Dict: + """Base server information fixture.""" + return { + "name": TEST_SERVER_NAME, + "serverId": TEST_SERVER_ID, + "metricsEnabled": True, + "createdTimestampMs": int(datetime.now(timezone.utc).timestamp() * 1000), + "version": "1.0.0", + "portForNewAccessKeys": 8388, + "hostnameForAccessKeys": "vpn.example.com", + "accessKeyDataLimit": None + } + + +@pytest.fixture +def access_key_data() -> Dict: + """Base access key data fixture.""" + return { + "id": 1, + "name": "Test Key", + "password": "test-password", + "port": 8388, + "method": "chacha20-ietf-poly1305", + "accessUrl": "ss://test-url", + "dataLimit": None + } + + +@pytest.fixture +def access_key_list_data(access_key_data) -> Dict: + """Access key list fixture.""" + return { + "accessKeys": [access_key_data] + } + + +@pytest.fixture +def metrics_data() -> Dict: + """Server metrics fixture.""" + return { + "bytesTransferredByUserId": { + "1": 1024 * 1024 * 100, # 100 MB + "2": 1024 * 1024 * 200 # 200 MB + } + } + + +@pytest.fixture +async def client() -> AsyncGenerator[AsyncOutlineClient, None]: + """Fixture for AsyncOutlineClient with mocked session.""" + client = AsyncOutlineClient( + TEST_API_URL, + TEST_CERT_SHA256, + json_format=True + ) + + # Create mock session + mock_session = MagicMock(spec=ClientSession) + mock_session.closed = False + client._session = mock_session + + yield client + + # Cleanup + if client.session and not client.session.closed: + await client.session.close() + + +@pytest.fixture +def mock_successful_response(): + """Fixture for successful API responses.""" + + def configure_response(data: dict): + return MockResponse(status=200, data=data) + + return configure_response + + +@pytest.fixture +def mock_error_response(): + """Fixture for error API responses.""" + + def configure_error(status_code: int, error_code: str, message: str): + return MockResponse( + status=status_code, + data={ + "code": error_code, + "message": message + } + ) + + return configure_error + + +# Test case helpers +async def assert_request_called_with( + client: AsyncOutlineClient, + method: str, + endpoint: str, + json: dict = None, + params: dict = None +): + """Helper to verify request parameters.""" + expected_url = f"{TEST_API_URL}/{endpoint.lstrip('/')}" + client.session.request.assert_called_once_with( + method, + expected_url, + json=json, + params=params, + raise_for_status=False + ) + + +@pytest.mark.asyncio +async def test_get_server_info( + client: AsyncOutlineClient, + server_info: Dict, + mock_successful_response +): + """Test get_server_info method.""" + # Configure mock response + client._session.request.return_value = mock_successful_response(server_info) + + # Make request + result = await client.get_server_info() + + # Verify request + await assert_request_called_with(client, "GET", "server") + + # Verify response + assert isinstance(result, dict) + assert result["name"] == TEST_SERVER_NAME + assert result["server_id"] == TEST_SERVER_ID + + +@pytest.mark.asyncio +async def test_create_access_key( + client: AsyncOutlineClient, + access_key_data: Dict, + mock_successful_response +): + """Test create_access_key method.""" + # Configure mock response + client._session.request.return_value = mock_successful_response(access_key_data) + + # Test data + key_name = "New Key" + port = 8389 + data_limit = DataLimit(bytes=1024 * 1024 * 1024) # 1 GB + + # Make request + result = await client.create_access_key( + name=key_name, + port=port, + limit=data_limit + ) + + # Verify request + await assert_request_called_with( + client, + "POST", + "access-keys", + json={ + "name": key_name, + "port": port, + "limit": {"bytes": data_limit.bytes} + } + ) + + # Verify response + assert isinstance(result, dict) + assert result["id"] == access_key_data["id"] + + +@pytest.mark.asyncio +async def test_get_metrics( + client: AsyncOutlineClient, + metrics_data: Dict, + mock_successful_response +): + """Test get_transfer_metrics method.""" + # Configure mock response + client._session.request.return_value = mock_successful_response(metrics_data) + + # Make request + result = await client.get_transfer_metrics() + + # Verify request + await assert_request_called_with( + client, + "GET", + "metrics/transfer", + params={"period": "monthly"} + ) + + # Verify response + assert isinstance(result, dict) + assert "bytes_transferred_by_user_id" in result + assert result["bytes_transferred_by_user_id"]["1"] == metrics_data["bytesTransferredByUserId"]["1"] + + +@pytest.mark.asyncio +async def test_error_handling( + client: AsyncOutlineClient, + mock_error_response +): + """Test API error handling.""" + # Configure error response + error_code = "forbidden" + error_message = "Access denied" + client._session.request.return_value = mock_error_response(403, error_code, error_message) + + # Verify error is raised + with pytest.raises(APIError) as exc_info: + await client.get_server_info() + + assert exc_info.value.status_code == 403 + assert error_code in str(exc_info.value) + assert error_message in str(exc_info.value) From ee85aba64710ee35d9cc496b839b1fcd7888a35d Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 17:52:47 +0500 Subject: [PATCH 16/24] test: fix TypeAlias error in Python 3.9 tests --- poetry.lock | 2 +- pyoutlineapi/__init__.py | 1 + pyoutlineapi/client.py | 23 ++++++----- pyoutlineapi/models.py | 1 + pyproject.toml | 1 + tests/test_outline_api.py | 85 +++++++++++++-------------------------- 6 files changed, 45 insertions(+), 68 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8abdbf8..9d9d044 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1293,4 +1293,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "9ce916bd51e0aa973c0eccd7ed78320588b95542802e99a2d8bb3e1f7b7a547b" +content-hash = "87d32253d010ad1b9f1df17e183e300693686936988138c69a43a43400a5be7b" diff --git a/pyoutlineapi/__init__.py b/pyoutlineapi/__init__.py index 271077c..ee972a6 100644 --- a/pyoutlineapi/__init__.py +++ b/pyoutlineapi/__init__.py @@ -11,6 +11,7 @@ Source code repository: https://github.com/orenlab/pyoutlineapi """ + import sys from typing import TYPE_CHECKING diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index ee19e51..9760c6e 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -11,11 +11,17 @@ Source code repository: https://github.com/orenlab/pyoutlineapi """ + from __future__ import annotations import binascii from functools import wraps -from typing import Any, Literal, TypeAlias, Union, overload, Optional, ParamSpec, TypeVar, Callable + +try: + from typing import TypeAlias +except ImportError: + from typing_extensions import TypeAlias +from typing import Any, Literal, Union, overload, Optional, ParamSpec, TypeVar, Callable from urllib.parse import urlparse import aiohttp @@ -35,8 +41,8 @@ ) # Type variables for decorator -P = ParamSpec('P') -T = TypeVar('T') +P = ParamSpec("P") +T = TypeVar("T") # Type aliases JsonDict: TypeAlias = dict[str, Any] @@ -106,7 +112,7 @@ async def __aenter__(self) -> AsyncOutlineClient: self._session = aiohttp.ClientSession( timeout=self._timeout, raise_for_status=False, - connector=aiohttp.TCPConnector(ssl=self._get_ssl_context()) + connector=aiohttp.TCPConnector(ssl=self._get_ssl_context()), ) return self @@ -142,10 +148,7 @@ async def _parse_response( @ensure_context async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: bool = True + self, response: ClientResponse, model: type[BaseModel], json_format: bool = True ) -> ResponseType: """ Parse and validate API response data. @@ -178,7 +181,9 @@ async def _handle_error_response(response: ClientResponse) -> None: error = ErrorResponse.model_validate(error_data) raise APIError(f"{error.code}: {error.message}", response.status) except ValueError: - raise APIError(f"HTTP {response.status}: {response.reason}", response.status) + raise APIError( + f"HTTP {response.status}: {response.reason}", response.status + ) @ensure_context async def _request( diff --git a/pyoutlineapi/models.py b/pyoutlineapi/models.py index d3af8c6..3ad3bce 100644 --- a/pyoutlineapi/models.py +++ b/pyoutlineapi/models.py @@ -11,6 +11,7 @@ Source code repository: https://github.com/orenlab/pyoutlineapi """ + from enum import Enum from typing import Optional diff --git a/pyproject.toml b/pyproject.toml index 4854ea0..69b525f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.9,<4.0" +typing-extensions = "^4.0.0" pydantic = "^2.9.2" aiohttp = "^3.11.11" diff --git a/tests/test_outline_api.py b/tests/test_outline_api.py index 7ec9bd6..1dd880d 100644 --- a/tests/test_outline_api.py +++ b/tests/test_outline_api.py @@ -6,9 +6,7 @@ from aiohttp import ClientSession from pyoutlineapi import AsyncOutlineClient, APIError -from pyoutlineapi.models import ( - DataLimit -) +from pyoutlineapi.models import DataLimit # Constants for testing TEST_API_URL = "https://example.com:1234/secret" @@ -45,7 +43,7 @@ def server_info() -> Dict: "version": "1.0.0", "portForNewAccessKeys": 8388, "hostnameForAccessKeys": "vpn.example.com", - "accessKeyDataLimit": None + "accessKeyDataLimit": None, } @@ -59,16 +57,14 @@ def access_key_data() -> Dict: "port": 8388, "method": "chacha20-ietf-poly1305", "accessUrl": "ss://test-url", - "dataLimit": None + "dataLimit": None, } @pytest.fixture def access_key_list_data(access_key_data) -> Dict: """Access key list fixture.""" - return { - "accessKeys": [access_key_data] - } + return {"accessKeys": [access_key_data]} @pytest.fixture @@ -77,7 +73,7 @@ def metrics_data() -> Dict: return { "bytesTransferredByUserId": { "1": 1024 * 1024 * 100, # 100 MB - "2": 1024 * 1024 * 200 # 200 MB + "2": 1024 * 1024 * 200, # 200 MB } } @@ -85,11 +81,7 @@ def metrics_data() -> Dict: @pytest.fixture async def client() -> AsyncGenerator[AsyncOutlineClient, None]: """Fixture for AsyncOutlineClient with mocked session.""" - client = AsyncOutlineClient( - TEST_API_URL, - TEST_CERT_SHA256, - json_format=True - ) + client = AsyncOutlineClient(TEST_API_URL, TEST_CERT_SHA256, json_format=True) # Create mock session mock_session = MagicMock(spec=ClientSession) @@ -119,11 +111,7 @@ def mock_error_response(): def configure_error(status_code: int, error_code: str, message: str): return MockResponse( - status=status_code, - data={ - "code": error_code, - "message": message - } + status=status_code, data={"code": error_code, "message": message} ) return configure_error @@ -131,28 +119,22 @@ def configure_error(status_code: int, error_code: str, message: str): # Test case helpers async def assert_request_called_with( - client: AsyncOutlineClient, - method: str, - endpoint: str, - json: dict = None, - params: dict = None + client: AsyncOutlineClient, + method: str, + endpoint: str, + json: dict = None, + params: dict = None, ): """Helper to verify request parameters.""" expected_url = f"{TEST_API_URL}/{endpoint.lstrip('/')}" client.session.request.assert_called_once_with( - method, - expected_url, - json=json, - params=params, - raise_for_status=False + method, expected_url, json=json, params=params, raise_for_status=False ) @pytest.mark.asyncio async def test_get_server_info( - client: AsyncOutlineClient, - server_info: Dict, - mock_successful_response + client: AsyncOutlineClient, server_info: Dict, mock_successful_response ): """Test get_server_info method.""" # Configure mock response @@ -172,9 +154,7 @@ async def test_get_server_info( @pytest.mark.asyncio async def test_create_access_key( - client: AsyncOutlineClient, - access_key_data: Dict, - mock_successful_response + client: AsyncOutlineClient, access_key_data: Dict, mock_successful_response ): """Test create_access_key method.""" # Configure mock response @@ -186,22 +166,14 @@ async def test_create_access_key( data_limit = DataLimit(bytes=1024 * 1024 * 1024) # 1 GB # Make request - result = await client.create_access_key( - name=key_name, - port=port, - limit=data_limit - ) + result = await client.create_access_key(name=key_name, port=port, limit=data_limit) # Verify request await assert_request_called_with( client, "POST", "access-keys", - json={ - "name": key_name, - "port": port, - "limit": {"bytes": data_limit.bytes} - } + json={"name": key_name, "port": port, "limit": {"bytes": data_limit.bytes}}, ) # Verify response @@ -211,9 +183,7 @@ async def test_create_access_key( @pytest.mark.asyncio async def test_get_metrics( - client: AsyncOutlineClient, - metrics_data: Dict, - mock_successful_response + client: AsyncOutlineClient, metrics_data: Dict, mock_successful_response ): """Test get_transfer_metrics method.""" # Configure mock response @@ -224,28 +194,27 @@ async def test_get_metrics( # Verify request await assert_request_called_with( - client, - "GET", - "metrics/transfer", - params={"period": "monthly"} + client, "GET", "metrics/transfer", params={"period": "monthly"} ) # Verify response assert isinstance(result, dict) assert "bytes_transferred_by_user_id" in result - assert result["bytes_transferred_by_user_id"]["1"] == metrics_data["bytesTransferredByUserId"]["1"] + assert ( + result["bytes_transferred_by_user_id"]["1"] + == metrics_data["bytesTransferredByUserId"]["1"] + ) @pytest.mark.asyncio -async def test_error_handling( - client: AsyncOutlineClient, - mock_error_response -): +async def test_error_handling(client: AsyncOutlineClient, mock_error_response): """Test API error handling.""" # Configure error response error_code = "forbidden" error_message = "Access denied" - client._session.request.return_value = mock_error_response(403, error_code, error_message) + client._session.request.return_value = mock_error_response( + 403, error_code, error_message + ) # Verify error is raised with pytest.raises(APIError) as exc_info: From 50ed84210b4f949f1644466a6edb077fff33af4e Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 17:57:38 +0500 Subject: [PATCH 17/24] chore(ci): fix module Flake8 not found errors --- .github/workflows/python_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index b141a7f..630b3e0 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -48,7 +48,7 @@ jobs: - name: Install dependencies if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction + run: poetry install --no-interaction --with dev - name: Run tests run: | From da1572b374aa2d785c6bd08fd4bc0f4ea46be109 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 17:59:43 +0500 Subject: [PATCH 18/24] chore(ci): fix module Flake8 not found errors --- poetry.lock | 55 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 ++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 9d9d044..5af4629 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,6 +340,23 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "flake8" +version = "7.1.1" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +groups = ["main", "dev"] +files = [ + {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, + {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" + [[package]] name = "frozenlist" version = "1.5.0" @@ -558,6 +575,18 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + [[package]] name = "multidict" version = "6.1.0" @@ -901,6 +930,18 @@ files = [ {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, ] +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + [[package]] name = "pydantic" version = "2.10.5" @@ -1035,6 +1076,18 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + [[package]] name = "pygments" version = "2.19.1" @@ -1293,4 +1346,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "87d32253d010ad1b9f1df17e183e300693686936988138c69a43a43400a5be7b" +content-hash = "6dab9a5c44b0a4c42a77d1e712ee983f4e191dd6dac9788993da2bf8113cd0ce" diff --git a/pyproject.toml b/pyproject.toml index 69b525f..118ffaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,10 @@ python = ">=3.9,<4.0" typing-extensions = "^4.0.0" pydantic = "^2.9.2" aiohttp = "^3.11.11" +flake8 = "^7.1.1" [tool.poetry.group.dev.dependencies] +flake8 = "^7.1.1" pytest = "^8.3.4" pytest-asyncio = "^0.25.2" pytest-cov = "^5.0.0" From b488b45ef2091e19ee951824f7b2dca3f69f213f Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 18:06:00 +0500 Subject: [PATCH 19/24] chore: remove Python 3.9 support --- .github/workflows/python_tests.yml | 9 +-------- pyoutlineapi/__init__.py | 2 +- pyoutlineapi/client.py | 7 +------ pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index 630b3e0..696103a 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -48,19 +48,12 @@ jobs: - name: Install dependencies if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --with dev + run: poetry install --no-interaction - name: Run tests run: | poetry run pytest --cov=./ --cov-report=xml - - name: Run linting - run: | - poetry run flake8 . --count --statistics \ - --max-line-length=88 \ - --extend-ignore=E203 \ - --max-complexity=10 - - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: diff --git a/pyoutlineapi/__init__.py b/pyoutlineapi/__init__.py index ee972a6..58dfac9 100644 --- a/pyoutlineapi/__init__.py +++ b/pyoutlineapi/__init__.py @@ -15,7 +15,7 @@ import sys from typing import TYPE_CHECKING -if sys.version_info < (3, 9): +if sys.version_info < (3, 10): raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher") from .client import AsyncOutlineClient, OutlineError, APIError diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index 9760c6e..bf86c4e 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -16,12 +16,7 @@ import binascii from functools import wraps - -try: - from typing import TypeAlias -except ImportError: - from typing_extensions import TypeAlias -from typing import Any, Literal, Union, overload, Optional, ParamSpec, TypeVar, Callable +from typing import Any, Literal, TypeAlias, Union, overload, Optional, ParamSpec, TypeVar, Callable from urllib.parse import urlparse import aiohttp diff --git a/pyproject.toml b/pyproject.toml index 118ffaa..9c1ec47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">=3.9,<4.0" +python = ">=3.10,<4.0" typing-extensions = "^4.0.0" pydantic = "^2.9.2" aiohttp = "^3.11.11" From c0dcfc003472b6932ce5020bbe8b372fbfcfc71e Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 18:07:57 +0500 Subject: [PATCH 20/24] chore: remove Python 3.9 support --- .github/workflows/python_tests.yml | 2 +- poetry.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index 696103a..d7e81d5 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + python-version: [ "3.10", "3.11", "3.12", "3.13" ] steps: - uses: actions/checkout@v4 diff --git a/poetry.lock b/poetry.lock index 5af4629..80d26b7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1345,5 +1345,5 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" -python-versions = ">=3.9,<4.0" -content-hash = "6dab9a5c44b0a4c42a77d1e712ee983f4e191dd6dac9788993da2bf8113cd0ce" +python-versions = ">=3.10,<4.0" +content-hash = "bcfca13b07a8a0405ce9231dc705f4dd15cc86c47764f3cbda91d67045f02318" From c1c1463ef48252b1de191ab0ee8acad1ef64d8a9 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 18:14:20 +0500 Subject: [PATCH 21/24] chore: remove typing-extensions from deps --- poetry.lock | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 80d26b7..c8a0c01 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1346,4 +1346,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "bcfca13b07a8a0405ce9231dc705f4dd15cc86c47764f3cbda91d67045f02318" +content-hash = "31e3d0112e6f1a25d2bb15d81a2cc1f9606f67a07a93414b44e8139ecc180a62" diff --git a/pyproject.toml b/pyproject.toml index 9c1ec47..c6eb33c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.10,<4.0" -typing-extensions = "^4.0.0" pydantic = "^2.9.2" aiohttp = "^3.11.11" flake8 = "^7.1.1" From 1354666da279c5cd816b06e35c4912e644349724 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 18:18:24 +0500 Subject: [PATCH 22/24] chore: added Codecov badge --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a388f5f..6a49fb0 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ models. [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![tests](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml/badge.svg)](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml) +[![codecov](https://codecov.io/gh/orenlab/pyoutlineapi/branch/development/graph/badge.svg?token=D0MPKCKFJQ)](https://codecov.io/gh/orenlab/pyoutlineapi) ![PyPI - Downloads](https://img.shields.io/pypi/dm/pyoutlineapi) ## Features @@ -133,14 +134,14 @@ from pyoutlineapi.models import MetricsPeriod async def get_metrics(): - async with AsyncOutlineClient(...) as client: - # Enable metrics collection - await client.set_metrics_status(True) - - # Get transfer metrics - metrics = await client.get_transfer_metrics(MetricsPeriod.MONTHLY) - for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items(): - print(f"User {user_id}: {bytes_transferred / 1024 ** 3:.2f} GB") + async with AsyncOutlineClient(...) as client: + # Enable metrics collection + await client.set_metrics_status(True) + + # Get transfer metrics + metrics = await client.get_transfer_metrics(MetricsPeriod.MONTHLY) + for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items(): + print(f"User {user_id}: {bytes_transferred / 1024 ** 3:.2f} GB") ``` ## Error Handling From 757bf0cb12febb26bd764b70628c8e4f5e6aad23 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 21:23:12 +0500 Subject: [PATCH 23/24] chore: small adjustments --- README.md | 13 +- docs/pyoutlineapi.html | 1649 ++++++++++++++++++++-------------------- docs/search.js | 2 +- pyoutlineapi/client.py | 96 +-- 4 files changed, 904 insertions(+), 856 deletions(-) diff --git a/README.md b/README.md index 6a49fb0..d929b49 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ models. - **SSL/TLS Security**: Certificate fingerprint verification for enhanced security - **Flexible Response Format**: Choose between Pydantic models or JSON responses - **Data Transfer Metrics**: Built-in support for monitoring server and key usage -- **Rate Limiting**: Built-in handling of API rate limits - **Context Manager Support**: Clean resource management with async context managers ## Installation @@ -86,6 +85,10 @@ client = AsyncOutlineClient( Create and manage access keys: ```python + +from pyoutlineapi import AsyncOutlineClient, DataLimit + + async def manage_keys(): async with AsyncOutlineClient(...) as client: # Create a key with data limit @@ -113,6 +116,10 @@ async def manage_keys(): Configure server settings: ```python + +from pyoutlineapi import AsyncOutlineClient + + async def configure_server(): async with AsyncOutlineClient(...) as client: # Update server name @@ -130,7 +137,7 @@ async def configure_server(): Monitor server usage: ```python -from pyoutlineapi.models import MetricsPeriod +from pyoutlineapi import AsyncOutlineClient, MetricsPeriod async def get_metrics(): @@ -149,7 +156,7 @@ async def get_metrics(): The client provides custom exceptions for different error scenarios: ```python -from pyoutlineapi import OutlineError, APIError +from pyoutlineapi import AsyncOutlineClient, OutlineError, APIError async def handle_errors(): diff --git a/docs/pyoutlineapi.html b/docs/pyoutlineapi.html index fdfbadb..6ce28e0 100644 --- a/docs/pyoutlineapi.html +++ b/docs/pyoutlineapi.html @@ -71,6 +71,9 @@

    API Documentation

  • remove_access_key_data_limit
  • +
  • + session +
  • @@ -293,7 +296,7 @@

    PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    -

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru +

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru All rights reserved.

    This software is licensed under the MIT License.

    @@ -328,64 +331,65 @@
    Source code repository:
    11Source code repository: 12 https://github.com/orenlab/pyoutlineapi 13""" -14import sys -15from typing import TYPE_CHECKING -16 -17if sys.version_info < (3, 10): -18 raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher") -19 -20from .client import AsyncOutlineClient, OutlineError, APIError -21 -22if TYPE_CHECKING: -23 from .models import ( -24 AccessKey, -25 AccessKeyCreateRequest, -26 AccessKeyList, -27 DataLimit, -28 ErrorResponse, -29 ExperimentalMetrics, -30 MetricsPeriod, -31 MetricsStatusResponse, -32 Server, -33 ServerMetrics, -34 ) -35 -36__version__: str = "0.2.0" -37__author__ = "Denis Rozhnovskiy" -38__email__ = "pytelemonbot@mail.ru" -39__license__ = "MIT" -40 -41PUBLIC_API = [ -42 "AsyncOutlineClient", -43 "OutlineError", -44 "APIError", -45 "AccessKey", -46 "AccessKeyCreateRequest", -47 "AccessKeyList", -48 "DataLimit", -49 "ErrorResponse", -50 "ExperimentalMetrics", -51 "MetricsPeriod", -52 "MetricsStatusResponse", -53 "Server", -54 "ServerMetrics", -55] -56 -57__all__ = PUBLIC_API -58 -59# Actual imports for runtime -60from .models import ( -61 AccessKey, -62 AccessKeyCreateRequest, -63 AccessKeyList, -64 DataLimit, -65 ErrorResponse, -66 ExperimentalMetrics, -67 MetricsPeriod, -68 MetricsStatusResponse, -69 Server, -70 ServerMetrics, -71) +14 +15import sys +16from typing import TYPE_CHECKING +17 +18if sys.version_info < (3, 10): +19 raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher") +20 +21from .client import AsyncOutlineClient, OutlineError, APIError +22 +23if TYPE_CHECKING: +24 from .models import ( +25 AccessKey, +26 AccessKeyCreateRequest, +27 AccessKeyList, +28 DataLimit, +29 ErrorResponse, +30 ExperimentalMetrics, +31 MetricsPeriod, +32 MetricsStatusResponse, +33 Server, +34 ServerMetrics, +35 ) +36 +37__version__: str = "0.2.0" +38__author__ = "Denis Rozhnovskiy" +39__email__ = "pytelemonbot@mail.ru" +40__license__ = "MIT" +41 +42PUBLIC_API = [ +43 "AsyncOutlineClient", +44 "OutlineError", +45 "APIError", +46 "AccessKey", +47 "AccessKeyCreateRequest", +48 "AccessKeyList", +49 "DataLimit", +50 "ErrorResponse", +51 "ExperimentalMetrics", +52 "MetricsPeriod", +53 "MetricsStatusResponse", +54 "Server", +55 "ServerMetrics", +56] +57 +58__all__ = PUBLIC_API +59 +60# Actual imports for runtime +61from .models import ( +62 AccessKey, +63 AccessKeyCreateRequest, +64 AccessKeyList, +65 DataLimit, +66 ErrorResponse, +67 ExperimentalMetrics, +68 MetricsPeriod, +69 MetricsStatusResponse, +70 Server, +71 ServerMetrics, +72)

    @@ -401,118 +405,117 @@
    Source code repository:
    -
     71class AsyncOutlineClient:
    - 72    """
    - 73    Asynchronous client for the Outline VPN Server API.
    - 74
    - 75    Args:
    - 76        api_url: Base URL for the Outline server API
    - 77        cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    - 78        json_format: Return raw JSON instead of Pydantic models
    - 79        timeout: Request timeout in seconds
    - 80
    - 81    Examples:
    - 82        >>> async def doo_something():
    - 83        ...     async with AsyncOutlineClient(
    - 84        ...         "https://example.com:1234/secret",
    - 85        ...         "ab12cd34..."
    - 86        ...     ) as client:
    - 87        ...         server_info = await client.get_server_info()
    - 88    """
    - 89
    - 90    def __init__(
    - 91            self,
    - 92            api_url: str,
    - 93            cert_sha256: str,
    - 94            *,
    - 95            json_format: bool = True,
    - 96            timeout: float = 30.0,
    - 97    ) -> None:
    - 98        self._api_url = api_url.rstrip("/")
    - 99        self._cert_sha256 = cert_sha256
    -100        self._json_format = json_format
    -101        self._timeout = aiohttp.ClientTimeout(total=timeout)
    -102        self._ssl_context: Optional[Fingerprint] = None
    -103        self._session: Optional[aiohttp.ClientSession] = None
    -104
    -105    async def __aenter__(self) -> AsyncOutlineClient:
    -106        """Set up client session for context manager."""
    -107        self._session = aiohttp.ClientSession(
    -108            timeout=self._timeout,
    -109            raise_for_status=False,
    -110            connector=aiohttp.TCPConnector(ssl=self._get_ssl_context())
    -111        )
    -112        return self
    -113
    -114    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    -115        """Clean up client session."""
    -116        if self._session:
    -117            await self._session.close()
    -118            self._session = None
    -119
    -120    @overload
    -121    async def _parse_response(
    -122            self,
    -123            response: ClientResponse,
    -124            model: type[BaseModel],
    -125            json_format: Literal[True],
    -126    ) -> JsonDict:
    -127        ...
    -128
    -129    @overload
    -130    async def _parse_response(
    -131            self,
    -132            response: ClientResponse,
    -133            model: type[BaseModel],
    -134            json_format: Literal[False],
    -135    ) -> BaseModel:
    -136        ...
    -137
    -138    @overload
    -139    async def _parse_response(
    -140            self, response: ClientResponse, model: type[BaseModel], json_format: bool
    -141    ) -> Union[JsonDict, BaseModel]:
    -142        ...
    -143
    -144    @ensure_context
    -145    async def _parse_response(
    -146            self,
    -147            response: ClientResponse,
    -148            model: type[BaseModel],
    -149            json_format: bool = True
    -150    ) -> ResponseType:
    -151        """
    -152        Parse and validate API response data.
    -153
    -154        Args:
    -155            response: API response to parse
    -156            model: Pydantic model for validation
    -157            json_format: Whether to return raw JSON
    -158
    -159        Returns:
    -160            Validated response data
    -161
    -162        Raises:
    -163            ValueError: If response validation fails
    -164        """
    -165        try:
    -166            data = await response.json()
    -167            validated = model.model_validate(data)
    -168            return validated.model_dump() if json_format else validated
    -169        except aiohttp.ContentTypeError as e:
    -170            raise ValueError("Invalid response format") from e
    -171        except Exception as e:
    -172            raise ValueError(f"Validation error: {e}") from e
    -173
    -174    @staticmethod
    -175    async def _handle_error_response(response: ClientResponse) -> None:
    -176        """Handle error responses from the API."""
    -177        try:
    -178            error_data = await response.json()
    -179            error = ErrorResponse.model_validate(error_data)
    -180            raise APIError(f"{error.code}: {error.message}", response.status)
    -181        except ValueError:
    -182            raise APIError(f"HTTP {response.status}: {response.reason}", response.status)
    +            
     72class AsyncOutlineClient:
    + 73    """
    + 74    Asynchronous client for the Outline VPN Server API.
    + 75
    + 76    Args:
    + 77        api_url: Base URL for the Outline server API
    + 78        cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    + 79        json_format: Return raw JSON instead of Pydantic models
    + 80        timeout: Request timeout in seconds
    + 81
    + 82    Examples:
    + 83        >>> async def doo_something():
    + 84        ...     async with AsyncOutlineClient(
    + 85        ...         "https://example.com:1234/secret",
    + 86        ...         "ab12cd34..."
    + 87        ...     ) as client:
    + 88        ...         server_info = await client.get_server_info()
    + 89    """
    + 90
    + 91    def __init__(
    + 92            self,
    + 93            api_url: str,
    + 94            cert_sha256: str,
    + 95            *,
    + 96            json_format: bool = True,
    + 97            timeout: float = 30.0,
    + 98    ) -> None:
    + 99        self._api_url = api_url.rstrip("/")
    +100        self._cert_sha256 = cert_sha256
    +101        self._json_format = json_format
    +102        self._timeout = aiohttp.ClientTimeout(total=timeout)
    +103        self._ssl_context: Optional[Fingerprint] = None
    +104        self._session: Optional[aiohttp.ClientSession] = None
    +105
    +106    async def __aenter__(self) -> AsyncOutlineClient:
    +107        """Set up client session for context manager."""
    +108        self._session = aiohttp.ClientSession(
    +109            timeout=self._timeout,
    +110            raise_for_status=False,
    +111            connector=aiohttp.TCPConnector(ssl=self._get_ssl_context()),
    +112        )
    +113        return self
    +114
    +115    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    +116        """Clean up client session."""
    +117        if self._session:
    +118            await self._session.close()
    +119            self._session = None
    +120
    +121    @overload
    +122    async def _parse_response(
    +123            self,
    +124            response: ClientResponse,
    +125            model: type[BaseModel],
    +126            json_format: Literal[True],
    +127    ) -> JsonDict:
    +128        ...
    +129
    +130    @overload
    +131    async def _parse_response(
    +132            self,
    +133            response: ClientResponse,
    +134            model: type[BaseModel],
    +135            json_format: Literal[False],
    +136    ) -> BaseModel:
    +137        ...
    +138
    +139    @overload
    +140    async def _parse_response(
    +141            self, response: ClientResponse, model: type[BaseModel], json_format: bool
    +142    ) -> Union[JsonDict, BaseModel]:
    +143        ...
    +144
    +145    @ensure_context
    +146    async def _parse_response(
    +147            self, response: ClientResponse, model: type[BaseModel], json_format: bool = True
    +148    ) -> ResponseType:
    +149        """
    +150        Parse and validate API response data.
    +151
    +152        Args:
    +153            response: API response to parse
    +154            model: Pydantic model for validation
    +155            json_format: Whether to return raw JSON
    +156
    +157        Returns:
    +158            Validated response data
    +159
    +160        Raises:
    +161            ValueError: If response validation fails
    +162        """
    +163        try:
    +164            data = await response.json()
    +165            validated = model.model_validate(data)
    +166            return validated.model_dump() if json_format else validated
    +167        except aiohttp.ContentTypeError as e:
    +168            raise ValueError("Invalid response format") from e
    +169        except Exception as e:
    +170            raise ValueError(f"Validation error: {e}") from e
    +171
    +172    @staticmethod
    +173    async def _handle_error_response(response: ClientResponse) -> None:
    +174        """Handle error responses from the API."""
    +175        try:
    +176            error_data = await response.json()
    +177            error = ErrorResponse.model_validate(error_data)
    +178            raise APIError(f"{error.code}: {error.message}", response.status)
    +179        except ValueError:
    +180            raise APIError(
    +181                f"HTTP {response.status}: {response.reason}", response.status
    +182            )
     183
     184    @ensure_context
     185    async def _request(
    @@ -664,295 +667,302 @@ 
    Source code repository:
    331 ... await client.set_default_port(8388) 332 333 """ -334 return await self._request( -335 "PUT", "server/port-for-new-access-keys", json={"port": port} -336 ) -337 -338 async def get_metrics_status(self) -> dict[str, Any] | BaseModel: -339 """ -340 Get whether metrics collection is enabled. -341 -342 Returns: -343 Current metrics collection status +334 if port < 1025 or port > 65535: +335 raise ValueError("Privileged ports are not allowed. Use range: 1025-65535") +336 +337 return await self._request( +338 "PUT", "server/port-for-new-access-keys", json={"port": port} +339 ) +340 +341 async def get_metrics_status(self) -> dict[str, Any] | BaseModel: +342 """ +343 Get whether metrics collection is enabled. 344 -345 Examples: -346 >>> async def doo_something(): -347 ... async with AsyncOutlineClient( -348 ... "https://example.com:1234/secret", -349 ... "ab12cd34..." -350 ... ) as client: -351 ... if await client.get_metrics_status(): -352 ... print("Metrics collection is enabled") -353 """ -354 response = await self._request("GET", "metrics/enabled") -355 data = await self._parse_response( -356 response, MetricsStatusResponse, json_format=self._json_format -357 ) -358 return data -359 -360 async def set_metrics_status(self, enabled: bool) -> bool: -361 """ -362 Enable or disable metrics collection. -363 -364 Args: -365 enabled: Whether to enable metrics +345 Returns: +346 Current metrics collection status +347 +348 Examples: +349 >>> async def doo_something(): +350 ... async with AsyncOutlineClient( +351 ... "https://example.com:1234/secret", +352 ... "ab12cd34..." +353 ... ) as client: +354 ... if await client.get_metrics_status(): +355 ... print("Metrics collection is enabled") +356 """ +357 response = await self._request("GET", "metrics/enabled") +358 data = await self._parse_response( +359 response, MetricsStatusResponse, json_format=self._json_format +360 ) +361 return data +362 +363 async def set_metrics_status(self, enabled: bool) -> bool: +364 """ +365 Enable or disable metrics collection. 366 -367 Returns: -368 True if successful +367 Args: +368 enabled: Whether to enable metrics 369 -370 Examples: -371 >>> async def doo_something(): -372 ... async with AsyncOutlineClient( -373 ... "https://example.com:1234/secret", -374 ... "ab12cd34..." -375 ... ) as client: -376 ... # Enable metrics -377 ... await client.set_metrics_status(True) -378 ... # Check new status -379 ... is_enabled = await client.get_metrics_status() -380 """ -381 return await self._request( -382 "PUT", "metrics/enabled", json={"metricsEnabled": enabled} -383 ) -384 -385 async def get_transfer_metrics( -386 self, period: MetricsPeriod = MetricsPeriod.MONTHLY -387 ) -> Union[JsonDict, ServerMetrics]: -388 """ -389 Get transfer metrics for specified period. -390 -391 Args: -392 period: Time period for metrics (DAILY, WEEKLY, or MONTHLY) +370 Returns: +371 True if successful +372 +373 Examples: +374 >>> async def doo_something(): +375 ... async with AsyncOutlineClient( +376 ... "https://example.com:1234/secret", +377 ... "ab12cd34..." +378 ... ) as client: +379 ... # Enable metrics +380 ... await client.set_metrics_status(True) +381 ... # Check new status +382 ... is_enabled = await client.get_metrics_status() +383 """ +384 return await self._request( +385 "PUT", "metrics/enabled", json={"metricsEnabled": enabled} +386 ) +387 +388 async def get_transfer_metrics( +389 self, period: MetricsPeriod = MetricsPeriod.MONTHLY +390 ) -> Union[JsonDict, ServerMetrics]: +391 """ +392 Get transfer metrics for specified period. 393 -394 Returns: -395 Transfer metrics data for each access key +394 Args: +395 period: Time period for metrics (DAILY, WEEKLY, or MONTHLY) 396 -397 Examples: -398 >>> async def doo_something(): -399 ... async with AsyncOutlineClient( -400 ... "https://example.com:1234/secret", -401 ... "ab12cd34..." -402 ... ) as client: -403 ... # Get monthly metrics -404 ... metrics = await client.get_transfer_metrics() -405 ... # Or get daily metrics -406 ... daily = await client.get_transfer_metrics(MetricsPeriod.DAILY) -407 ... for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items(): -408 ... print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB") -409 """ -410 response = await self._request( -411 "GET", "metrics/transfer", params={"period": period.value} -412 ) -413 return await self._parse_response( -414 response, ServerMetrics, json_format=self._json_format +397 Returns: +398 Transfer metrics data for each access key +399 +400 Examples: +401 >>> async def doo_something(): +402 ... async with AsyncOutlineClient( +403 ... "https://example.com:1234/secret", +404 ... "ab12cd34..." +405 ... ) as client: +406 ... # Get monthly metrics +407 ... metrics = await client.get_transfer_metrics() +408 ... # Or get daily metrics +409 ... daily = await client.get_transfer_metrics(MetricsPeriod.DAILY) +410 ... for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items(): +411 ... print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB") +412 """ +413 response = await self._request( +414 "GET", "metrics/transfer", params={"period": period.value} 415 ) -416 -417 async def create_access_key( -418 self, -419 *, -420 name: Optional[str] = None, -421 password: Optional[str] = None, -422 port: Optional[int] = None, -423 method: Optional[str] = None, -424 limit: Optional[DataLimit] = None, -425 ) -> Union[JsonDict, AccessKey]: -426 """ -427 Create a new access key. -428 -429 Args: -430 name: Optional key name -431 password: Optional password -432 port: Optional port number (1-65535) -433 method: Optional encryption method -434 limit: Optional data transfer limit -435 -436 Returns: -437 New access key details +416 return await self._parse_response( +417 response, ServerMetrics, json_format=self._json_format +418 ) +419 +420 async def create_access_key( +421 self, +422 *, +423 name: Optional[str] = None, +424 password: Optional[str] = None, +425 port: Optional[int] = None, +426 method: Optional[str] = None, +427 limit: Optional[DataLimit] = None, +428 ) -> Union[JsonDict, AccessKey]: +429 """ +430 Create a new access key. +431 +432 Args: +433 name: Optional key name +434 password: Optional password +435 port: Optional port number (1-65535) +436 method: Optional encryption method +437 limit: Optional data transfer limit 438 -439 Examples: -440 >>> async def doo_something(): -441 ... async with AsyncOutlineClient( -442 ... "https://example.com:1234/secret", -443 ... "ab12cd34..." -444 ... ) as client: -445 ... # Create basic key -446 ... key = await client.create_access_key(name="User 1") -447 ... -448 ... # Create key with data limit -449 ... _limit = DataLimit(bytes=5 * 1024**3) # 5 GB -450 ... key = await client.create_access_key( -451 ... name="Limited User", -452 ... port=8388, -453 ... limit=_limit -454 ... ) -455 ... print(f"Created key: {key.access_url}") -456 """ -457 request = AccessKeyCreateRequest( -458 name=name, password=password, port=port, method=method, limit=limit -459 ) -460 response = await self._request( -461 "POST", "access-keys", json=request.model_dump(exclude_none=True) +439 Returns: +440 New access key details +441 +442 Examples: +443 >>> async def doo_something(): +444 ... async with AsyncOutlineClient( +445 ... "https://example.com:1234/secret", +446 ... "ab12cd34..." +447 ... ) as client: +448 ... # Create basic key +449 ... key = await client.create_access_key(name="User 1") +450 ... +451 ... # Create key with data limit +452 ... _limit = DataLimit(bytes=5 * 1024**3) # 5 GB +453 ... key = await client.create_access_key( +454 ... name="Limited User", +455 ... port=8388, +456 ... limit=_limit +457 ... ) +458 ... print(f"Created key: {key.access_url}") +459 """ +460 request = AccessKeyCreateRequest( +461 name=name, password=password, port=port, method=method, limit=limit 462 ) -463 return await self._parse_response( -464 response, AccessKey, json_format=self._json_format +463 response = await self._request( +464 "POST", "access-keys", json=request.model_dump(exclude_none=True) 465 ) -466 -467 async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]: -468 """ -469 Get all access keys. -470 -471 Returns: -472 List of all access keys +466 return await self._parse_response( +467 response, AccessKey, json_format=self._json_format +468 ) +469 +470 async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]: +471 """ +472 Get all access keys. 473 -474 Examples: -475 >>> async def doo_something(): -476 ... async with AsyncOutlineClient( -477 ... "https://example.com:1234/secret", -478 ... "ab12cd34..." -479 ... ) as client: -480 ... keys = await client.get_access_keys() -481 ... for key in keys.access_keys: -482 ... print(f"Key {key.id}: {key.name or 'unnamed'}") -483 ... if key.data_limit: -484 ... print(f" Limit: {key.data_limit.bytes / 1024**3:.1f} GB") -485 """ -486 response = await self._request("GET", "access-keys") -487 return await self._parse_response( -488 response, AccessKeyList, json_format=self._json_format -489 ) -490 -491 async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]: -492 """ -493 Get specific access key. -494 -495 Args: -496 key_id: Access key ID +474 Returns: +475 List of all access keys +476 +477 Examples: +478 >>> async def doo_something(): +479 ... async with AsyncOutlineClient( +480 ... "https://example.com:1234/secret", +481 ... "ab12cd34..." +482 ... ) as client: +483 ... keys = await client.get_access_keys() +484 ... for key in keys.access_keys: +485 ... print(f"Key {key.id}: {key.name or 'unnamed'}") +486 ... if key.data_limit: +487 ... print(f" Limit: {key.data_limit.bytes / 1024**3:.1f} GB") +488 """ +489 response = await self._request("GET", "access-keys") +490 return await self._parse_response( +491 response, AccessKeyList, json_format=self._json_format +492 ) +493 +494 async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]: +495 """ +496 Get specific access key. 497 -498 Returns: -499 Access key details +498 Args: +499 key_id: Access key ID 500 -501 Raises: -502 APIError: If key doesn't exist +501 Returns: +502 Access key details 503 -504 Examples: -505 >>> async def doo_something(): -506 ... async with AsyncOutlineClient( -507 ... "https://example.com:1234/secret", -508 ... "ab12cd34..." -509 ... ) as client: -510 ... key = await client.get_access_key(1) -511 ... print(f"Port: {key.port}") -512 ... print(f"URL: {key.access_url}") -513 """ -514 response = await self._request("GET", f"access-keys/{key_id}") -515 return await self._parse_response( -516 response, AccessKey, json_format=self._json_format -517 ) -518 -519 async def rename_access_key(self, key_id: int, name: str) -> bool: -520 """ -521 Rename access key. -522 -523 Args: -524 key_id: Access key ID -525 name: New name -526 -527 Returns: -528 True if successful +504 Raises: +505 APIError: If key doesn't exist +506 +507 Examples: +508 >>> async def doo_something(): +509 ... async with AsyncOutlineClient( +510 ... "https://example.com:1234/secret", +511 ... "ab12cd34..." +512 ... ) as client: +513 ... key = await client.get_access_key(1) +514 ... print(f"Port: {key.port}") +515 ... print(f"URL: {key.access_url}") +516 """ +517 response = await self._request("GET", f"access-keys/{key_id}") +518 return await self._parse_response( +519 response, AccessKey, json_format=self._json_format +520 ) +521 +522 async def rename_access_key(self, key_id: int, name: str) -> bool: +523 """ +524 Rename access key. +525 +526 Args: +527 key_id: Access key ID +528 name: New name 529 -530 Raises: -531 APIError: If key doesn't exist +530 Returns: +531 True if successful 532 -533 Examples: -534 >>> async def doo_something(): -535 ... async with AsyncOutlineClient( -536 ... "https://example.com:1234/secret", -537 ... "ab12cd34..." -538 ... ) as client: -539 ... # Rename key -540 ... await client.rename_access_key(1, "Alice") -541 ... -542 ... # Verify new name -543 ... key = await client.get_access_key(1) -544 ... assert key.name == "Alice" -545 """ -546 return await self._request( -547 "PUT", f"access-keys/{key_id}/name", json={"name": name} -548 ) -549 -550 async def delete_access_key(self, key_id: int) -> bool: -551 """ -552 Delete access key. -553 -554 Args: -555 key_id: Access key ID +533 Raises: +534 APIError: If key doesn't exist +535 +536 Examples: +537 >>> async def doo_something(): +538 ... async with AsyncOutlineClient( +539 ... "https://example.com:1234/secret", +540 ... "ab12cd34..." +541 ... ) as client: +542 ... # Rename key +543 ... await client.rename_access_key(1, "Alice") +544 ... +545 ... # Verify new name +546 ... key = await client.get_access_key(1) +547 ... assert key.name == "Alice" +548 """ +549 return await self._request( +550 "PUT", f"access-keys/{key_id}/name", json={"name": name} +551 ) +552 +553 async def delete_access_key(self, key_id: int) -> bool: +554 """ +555 Delete access key. 556 -557 Returns: -558 True if successful +557 Args: +558 key_id: Access key ID 559 -560 Raises: -561 APIError: If key doesn't exist +560 Returns: +561 True if successful 562 -563 Examples: -564 >>> async def doo_something(): -565 ... async with AsyncOutlineClient( -566 ... "https://example.com:1234/secret", -567 ... "ab12cd34..." -568 ... ) as client: -569 ... if await client.delete_access_key(1): -570 ... print("Key deleted") -571 -572 """ -573 return await self._request("DELETE", f"access-keys/{key_id}") +563 Raises: +564 APIError: If key doesn't exist +565 +566 Examples: +567 >>> async def doo_something(): +568 ... async with AsyncOutlineClient( +569 ... "https://example.com:1234/secret", +570 ... "ab12cd34..." +571 ... ) as client: +572 ... if await client.delete_access_key(1): +573 ... print("Key deleted") 574 -575 async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool: -576 """ -577 Set data transfer limit for access key. -578 -579 Args: -580 key_id: Access key ID -581 bytes_limit: Limit in bytes (must be positive) -582 -583 Returns: -584 True if successful +575 """ +576 return await self._request("DELETE", f"access-keys/{key_id}") +577 +578 async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool: +579 """ +580 Set data transfer limit for access key. +581 +582 Args: +583 key_id: Access key ID +584 bytes_limit: Limit in bytes (must be positive) 585 -586 Raises: -587 APIError: If key doesn't exist or limit is invalid +586 Returns: +587 True if successful 588 -589 Examples: -590 >>> async def doo_something(): -591 ... async with AsyncOutlineClient( -592 ... "https://example.com:1234/secret", -593 ... "ab12cd34..." -594 ... ) as client: -595 ... # Set 5 GB limit -596 ... limit = 5 * 1024**3 # 5 GB in bytes -597 ... await client.set_access_key_data_limit(1, limit) -598 ... -599 ... # Verify limit -600 ... key = await client.get_access_key(1) -601 ... assert key.data_limit and key.data_limit.bytes == limit -602 """ -603 return await self._request( -604 "PUT", -605 f"access-keys/{key_id}/data-limit", -606 json={"limit": {"bytes": bytes_limit}}, -607 ) -608 -609 async def remove_access_key_data_limit(self, key_id: str) -> bool: -610 """ -611 Remove data transfer limit from access key. -612 -613 Args: -614 key_id: Access key ID +589 Raises: +590 APIError: If key doesn't exist or limit is invalid +591 +592 Examples: +593 >>> async def doo_something(): +594 ... async with AsyncOutlineClient( +595 ... "https://example.com:1234/secret", +596 ... "ab12cd34..." +597 ... ) as client: +598 ... # Set 5 GB limit +599 ... limit = 5 * 1024**3 # 5 GB in bytes +600 ... await client.set_access_key_data_limit(1, limit) +601 ... +602 ... # Verify limit +603 ... key = await client.get_access_key(1) +604 ... assert key.data_limit and key.data_limit.bytes == limit +605 """ +606 return await self._request( +607 "PUT", +608 f"access-keys/{key_id}/data-limit", +609 json={"limit": {"bytes": bytes_limit}}, +610 ) +611 +612 async def remove_access_key_data_limit(self, key_id: int) -> bool: +613 """ +614 Remove data transfer limit from access key. 615 -616 Returns: -617 True if successful +616 Args: +617 key_id: Access key ID 618 -619 Raises: -620 APIError: If key doesn't exist -621 """ -622 return await self._request("DELETE", f"access-keys/{key_id}/data-limit") +619 Returns: +620 True if successful +621 +622 Raises: +623 APIError: If key doesn't exist +624 """ +625 return await self._request("DELETE", f"access-keys/{key_id}/data-limit") +626 +627 @property +628 def session(self): +629 return self._session
    @@ -993,20 +1003,20 @@
    Examples:
    -
     90    def __init__(
    - 91            self,
    - 92            api_url: str,
    - 93            cert_sha256: str,
    - 94            *,
    - 95            json_format: bool = True,
    - 96            timeout: float = 30.0,
    - 97    ) -> None:
    - 98        self._api_url = api_url.rstrip("/")
    - 99        self._cert_sha256 = cert_sha256
    -100        self._json_format = json_format
    -101        self._timeout = aiohttp.ClientTimeout(total=timeout)
    -102        self._ssl_context: Optional[Fingerprint] = None
    -103        self._session: Optional[aiohttp.ClientSession] = None
    +            
     91    def __init__(
    + 92            self,
    + 93            api_url: str,
    + 94            cert_sha256: str,
    + 95            *,
    + 96            json_format: bool = True,
    + 97            timeout: float = 30.0,
    + 98    ) -> None:
    + 99        self._api_url = api_url.rstrip("/")
    +100        self._cert_sha256 = cert_sha256
    +101        self._json_format = json_format
    +102        self._timeout = aiohttp.ClientTimeout(total=timeout)
    +103        self._ssl_context: Optional[Fingerprint] = None
    +104        self._session: Optional[aiohttp.ClientSession] = None
     
    @@ -1253,9 +1263,12 @@
    Examples:
    331 ... await client.set_default_port(8388) 332 333 """ -334 return await self._request( -335 "PUT", "server/port-for-new-access-keys", json={"port": port} -336 ) +334 if port < 1025 or port > 65535: +335 raise ValueError("Privileged ports are not allowed. Use range: 1025-65535") +336 +337 return await self._request( +338 "PUT", "server/port-for-new-access-keys", json={"port": port} +339 )
    @@ -1307,27 +1320,27 @@
    Examples:
    -
    338    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
    -339        """
    -340        Get whether metrics collection is enabled.
    -341
    -342        Returns:
    -343            Current metrics collection status
    +            
    341    async def get_metrics_status(self) -> dict[str, Any] | BaseModel:
    +342        """
    +343        Get whether metrics collection is enabled.
     344
    -345        Examples:
    -346            >>> async def doo_something():
    -347            ...     async with AsyncOutlineClient(
    -348            ...         "https://example.com:1234/secret",
    -349            ...         "ab12cd34..."
    -350            ...     ) as client:
    -351            ...         if await client.get_metrics_status():
    -352            ...             print("Metrics collection is enabled")
    -353        """
    -354        response = await self._request("GET", "metrics/enabled")
    -355        data = await self._parse_response(
    -356            response, MetricsStatusResponse, json_format=self._json_format
    -357        )
    -358        return data
    +345        Returns:
    +346            Current metrics collection status
    +347
    +348        Examples:
    +349            >>> async def doo_something():
    +350            ...     async with AsyncOutlineClient(
    +351            ...         "https://example.com:1234/secret",
    +352            ...         "ab12cd34..."
    +353            ...     ) as client:
    +354            ...         if await client.get_metrics_status():
    +355            ...             print("Metrics collection is enabled")
    +356        """
    +357        response = await self._request("GET", "metrics/enabled")
    +358        data = await self._parse_response(
    +359            response, MetricsStatusResponse, json_format=self._json_format
    +360        )
    +361        return data
     
    @@ -1368,30 +1381,30 @@
    Examples:
    -
    360    async def set_metrics_status(self, enabled: bool) -> bool:
    -361        """
    -362        Enable or disable metrics collection.
    -363
    -364        Args:
    -365            enabled: Whether to enable metrics
    +            
    363    async def set_metrics_status(self, enabled: bool) -> bool:
    +364        """
    +365        Enable or disable metrics collection.
     366
    -367        Returns:
    -368            True if successful
    +367        Args:
    +368            enabled: Whether to enable metrics
     369
    -370        Examples:
    -371            >>> async def doo_something():
    -372            ...     async with AsyncOutlineClient(
    -373            ...         "https://example.com:1234/secret",
    -374            ...         "ab12cd34..."
    -375            ...     ) as client:
    -376            ...         # Enable metrics
    -377            ...         await client.set_metrics_status(True)
    -378            ...         # Check new status
    -379            ...         is_enabled = await client.get_metrics_status()
    -380        """
    -381        return await self._request(
    -382            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
    -383        )
    +370        Returns:
    +371            True if successful
    +372
    +373        Examples:
    +374            >>> async def doo_something():
    +375            ...     async with AsyncOutlineClient(
    +376            ...         "https://example.com:1234/secret",
    +377            ...         "ab12cd34..."
    +378            ...     ) as client:
    +379            ...         # Enable metrics
    +380            ...         await client.set_metrics_status(True)
    +381            ...         # Check new status
    +382            ...         is_enabled = await client.get_metrics_status()
    +383        """
    +384        return await self._request(
    +385            "PUT", "metrics/enabled", json={"metricsEnabled": enabled}
    +386        )
     
    @@ -1440,37 +1453,37 @@
    Examples:
    -
    385    async def get_transfer_metrics(
    -386            self, period: MetricsPeriod = MetricsPeriod.MONTHLY
    -387    ) -> Union[JsonDict, ServerMetrics]:
    -388        """
    -389        Get transfer metrics for specified period.
    -390
    -391        Args:
    -392            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    +            
    388    async def get_transfer_metrics(
    +389            self, period: MetricsPeriod = MetricsPeriod.MONTHLY
    +390    ) -> Union[JsonDict, ServerMetrics]:
    +391        """
    +392        Get transfer metrics for specified period.
     393
    -394        Returns:
    -395            Transfer metrics data for each access key
    +394        Args:
    +395            period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
     396
    -397        Examples:
    -398            >>> async def doo_something():
    -399            ...     async with AsyncOutlineClient(
    -400            ...         "https://example.com:1234/secret",
    -401            ...         "ab12cd34..."
    -402            ...     ) as client:
    -403            ...         # Get monthly metrics
    -404            ...         metrics = await client.get_transfer_metrics()
    -405            ...         # Or get daily metrics
    -406            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
    -407            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
    -408            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    -409        """
    -410        response = await self._request(
    -411            "GET", "metrics/transfer", params={"period": period.value}
    -412        )
    -413        return await self._parse_response(
    -414            response, ServerMetrics, json_format=self._json_format
    +397        Returns:
    +398            Transfer metrics data for each access key
    +399
    +400        Examples:
    +401            >>> async def doo_something():
    +402            ...     async with AsyncOutlineClient(
    +403            ...         "https://example.com:1234/secret",
    +404            ...         "ab12cd34..."
    +405            ...     ) as client:
    +406            ...         # Get monthly metrics
    +407            ...         metrics = await client.get_transfer_metrics()
    +408            ...         # Or get daily metrics
    +409            ...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)
    +410            ...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():
    +411            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    +412        """
    +413        response = await self._request(
    +414            "GET", "metrics/transfer", params={"period": period.value}
     415        )
    +416        return await self._parse_response(
    +417            response, ServerMetrics, json_format=self._json_format
    +418        )
     
    @@ -1521,55 +1534,55 @@
    Examples:
    -
    417    async def create_access_key(
    -418            self,
    -419            *,
    -420            name: Optional[str] = None,
    -421            password: Optional[str] = None,
    -422            port: Optional[int] = None,
    -423            method: Optional[str] = None,
    -424            limit: Optional[DataLimit] = None,
    -425    ) -> Union[JsonDict, AccessKey]:
    -426        """
    -427        Create a new access key.
    -428
    -429        Args:
    -430            name: Optional key name
    -431            password: Optional password
    -432            port: Optional port number (1-65535)
    -433            method: Optional encryption method
    -434            limit: Optional data transfer limit
    -435
    -436        Returns:
    -437            New access key details
    +            
    420    async def create_access_key(
    +421            self,
    +422            *,
    +423            name: Optional[str] = None,
    +424            password: Optional[str] = None,
    +425            port: Optional[int] = None,
    +426            method: Optional[str] = None,
    +427            limit: Optional[DataLimit] = None,
    +428    ) -> Union[JsonDict, AccessKey]:
    +429        """
    +430        Create a new access key.
    +431
    +432        Args:
    +433            name: Optional key name
    +434            password: Optional password
    +435            port: Optional port number (1-65535)
    +436            method: Optional encryption method
    +437            limit: Optional data transfer limit
     438
    -439        Examples:
    -440            >>> async def doo_something():
    -441            ...     async with AsyncOutlineClient(
    -442            ...         "https://example.com:1234/secret",
    -443            ...         "ab12cd34..."
    -444            ...     ) as client:
    -445            ...         # Create basic key
    -446            ...         key = await client.create_access_key(name="User 1")
    -447            ...
    -448            ...         # Create key with data limit
    -449            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
    -450            ...         key = await client.create_access_key(
    -451            ...             name="Limited User",
    -452            ...             port=8388,
    -453            ...             limit=_limit
    -454            ...         )
    -455            ...         print(f"Created key: {key.access_url}")
    -456        """
    -457        request = AccessKeyCreateRequest(
    -458            name=name, password=password, port=port, method=method, limit=limit
    -459        )
    -460        response = await self._request(
    -461            "POST", "access-keys", json=request.model_dump(exclude_none=True)
    +439        Returns:
    +440            New access key details
    +441
    +442        Examples:
    +443            >>> async def doo_something():
    +444            ...     async with AsyncOutlineClient(
    +445            ...         "https://example.com:1234/secret",
    +446            ...         "ab12cd34..."
    +447            ...     ) as client:
    +448            ...         # Create basic key
    +449            ...         key = await client.create_access_key(name="User 1")
    +450            ...
    +451            ...         # Create key with data limit
    +452            ...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB
    +453            ...         key = await client.create_access_key(
    +454            ...             name="Limited User",
    +455            ...             port=8388,
    +456            ...             limit=_limit
    +457            ...         )
    +458            ...         print(f"Created key: {key.access_url}")
    +459        """
    +460        request = AccessKeyCreateRequest(
    +461            name=name, password=password, port=port, method=method, limit=limit
     462        )
    -463        return await self._parse_response(
    -464            response, AccessKey, json_format=self._json_format
    +463        response = await self._request(
    +464            "POST", "access-keys", json=request.model_dump(exclude_none=True)
     465        )
    +466        return await self._parse_response(
    +467            response, AccessKey, json_format=self._json_format
    +468        )
     
    @@ -1629,29 +1642,29 @@
    Examples:
    -
    467    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
    -468        """
    -469        Get all access keys.
    -470
    -471        Returns:
    -472            List of all access keys
    +            
    470    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
    +471        """
    +472        Get all access keys.
     473
    -474        Examples:
    -475            >>> async def doo_something():
    -476            ...     async with AsyncOutlineClient(
    -477            ...         "https://example.com:1234/secret",
    -478            ...         "ab12cd34..."
    -479            ...     ) as client:
    -480            ...         keys = await client.get_access_keys()
    -481            ...         for key in keys.access_keys:
    -482            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    -483            ...             if key.data_limit:
    -484            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    -485        """
    -486        response = await self._request("GET", "access-keys")
    -487        return await self._parse_response(
    -488            response, AccessKeyList, json_format=self._json_format
    -489        )
    +474        Returns:
    +475            List of all access keys
    +476
    +477        Examples:
    +478            >>> async def doo_something():
    +479            ...     async with AsyncOutlineClient(
    +480            ...         "https://example.com:1234/secret",
    +481            ...         "ab12cd34..."
    +482            ...     ) as client:
    +483            ...         keys = await client.get_access_keys()
    +484            ...         for key in keys.access_keys:
    +485            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    +486            ...             if key.data_limit:
    +487            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    +488        """
    +489        response = await self._request("GET", "access-keys")
    +490        return await self._parse_response(
    +491            response, AccessKeyList, json_format=self._json_format
    +492        )
     
    @@ -1695,33 +1708,33 @@
    Examples:
    -
    491    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
    -492        """
    -493        Get specific access key.
    -494
    -495        Args:
    -496            key_id: Access key ID
    +            
    494    async def get_access_key(self, key_id: int) -> Union[JsonDict, AccessKey]:
    +495        """
    +496        Get specific access key.
     497
    -498        Returns:
    -499            Access key details
    +498        Args:
    +499            key_id: Access key ID
     500
    -501        Raises:
    -502            APIError: If key doesn't exist
    +501        Returns:
    +502            Access key details
     503
    -504        Examples:
    -505            >>> async def doo_something():
    -506            ...     async with AsyncOutlineClient(
    -507            ...         "https://example.com:1234/secret",
    -508            ...         "ab12cd34..."
    -509            ...     ) as client:
    -510            ...         key = await client.get_access_key(1)
    -511            ...         print(f"Port: {key.port}")
    -512            ...         print(f"URL: {key.access_url}")
    -513        """
    -514        response = await self._request("GET", f"access-keys/{key_id}")
    -515        return await self._parse_response(
    -516            response, AccessKey, json_format=self._json_format
    -517        )
    +504        Raises:
    +505            APIError: If key doesn't exist
    +506
    +507        Examples:
    +508            >>> async def doo_something():
    +509            ...     async with AsyncOutlineClient(
    +510            ...         "https://example.com:1234/secret",
    +511            ...         "ab12cd34..."
    +512            ...     ) as client:
    +513            ...         key = await client.get_access_key(1)
    +514            ...         print(f"Port: {key.port}")
    +515            ...         print(f"URL: {key.access_url}")
    +516        """
    +517        response = await self._request("GET", f"access-keys/{key_id}")
    +518        return await self._parse_response(
    +519            response, AccessKey, json_format=self._json_format
    +520        )
     
    @@ -1775,36 +1788,36 @@
    Examples:
    -
    519    async def rename_access_key(self, key_id: int, name: str) -> bool:
    -520        """
    -521        Rename access key.
    -522
    -523        Args:
    -524            key_id: Access key ID
    -525            name: New name
    -526
    -527        Returns:
    -528            True if successful
    +            
    522    async def rename_access_key(self, key_id: int, name: str) -> bool:
    +523        """
    +524        Rename access key.
    +525
    +526        Args:
    +527            key_id: Access key ID
    +528            name: New name
     529
    -530        Raises:
    -531            APIError: If key doesn't exist
    +530        Returns:
    +531            True if successful
     532
    -533        Examples:
    -534            >>> async def doo_something():
    -535            ...     async with AsyncOutlineClient(
    -536            ...         "https://example.com:1234/secret",
    -537            ...         "ab12cd34..."
    -538            ...     ) as client:
    -539            ...         # Rename key
    -540            ...         await client.rename_access_key(1, "Alice")
    -541            ...
    -542            ...         # Verify new name
    -543            ...         key = await client.get_access_key(1)
    -544            ...         assert key.name == "Alice"
    -545        """
    -546        return await self._request(
    -547            "PUT", f"access-keys/{key_id}/name", json={"name": name}
    -548        )
    +533        Raises:
    +534            APIError: If key doesn't exist
    +535
    +536        Examples:
    +537            >>> async def doo_something():
    +538            ...     async with AsyncOutlineClient(
    +539            ...         "https://example.com:1234/secret",
    +540            ...         "ab12cd34..."
    +541            ...     ) as client:
    +542            ...         # Rename key
    +543            ...         await client.rename_access_key(1, "Alice")
    +544            ...
    +545            ...         # Verify new name
    +546            ...         key = await client.get_access_key(1)
    +547            ...         assert key.name == "Alice"
    +548        """
    +549        return await self._request(
    +550            "PUT", f"access-keys/{key_id}/name", json={"name": name}
    +551        )
     
    @@ -1862,30 +1875,30 @@
    Examples:
    -
    550    async def delete_access_key(self, key_id: int) -> bool:
    -551        """
    -552        Delete access key.
    -553
    -554        Args:
    -555            key_id: Access key ID
    +            
    553    async def delete_access_key(self, key_id: int) -> bool:
    +554        """
    +555        Delete access key.
     556
    -557        Returns:
    -558            True if successful
    +557        Args:
    +558            key_id: Access key ID
     559
    -560        Raises:
    -561            APIError: If key doesn't exist
    +560        Returns:
    +561            True if successful
     562
    -563        Examples:
    -564            >>> async def doo_something():
    -565            ...     async with AsyncOutlineClient(
    -566            ...         "https://example.com:1234/secret",
    -567            ...         "ab12cd34..."
    -568            ...     ) as client:
    -569            ...         if await client.delete_access_key(1):
    -570            ...             print("Key deleted")
    -571
    -572        """
    -573        return await self._request("DELETE", f"access-keys/{key_id}")
    +563        Raises:
    +564            APIError: If key doesn't exist
    +565
    +566        Examples:
    +567            >>> async def doo_something():
    +568            ...     async with AsyncOutlineClient(
    +569            ...         "https://example.com:1234/secret",
    +570            ...         "ab12cd34..."
    +571            ...     ) as client:
    +572            ...         if await client.delete_access_key(1):
    +573            ...             print("Key deleted")
    +574
    +575        """
    +576        return await self._request("DELETE", f"access-keys/{key_id}")
     
    @@ -1938,39 +1951,39 @@
    Examples:
    -
    575    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
    -576        """
    -577        Set data transfer limit for access key.
    -578
    -579        Args:
    -580            key_id: Access key ID
    -581            bytes_limit: Limit in bytes (must be positive)
    -582
    -583        Returns:
    -584            True if successful
    +            
    578    async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool:
    +579        """
    +580        Set data transfer limit for access key.
    +581
    +582        Args:
    +583            key_id: Access key ID
    +584            bytes_limit: Limit in bytes (must be positive)
     585
    -586        Raises:
    -587            APIError: If key doesn't exist or limit is invalid
    +586        Returns:
    +587            True if successful
     588
    -589        Examples:
    -590            >>> async def doo_something():
    -591            ...     async with AsyncOutlineClient(
    -592            ...         "https://example.com:1234/secret",
    -593            ...         "ab12cd34..."
    -594            ...     ) as client:
    -595            ...         # Set 5 GB limit
    -596            ...         limit = 5 * 1024**3  # 5 GB in bytes
    -597            ...         await client.set_access_key_data_limit(1, limit)
    -598            ...
    -599            ...         # Verify limit
    -600            ...         key = await client.get_access_key(1)
    -601            ...         assert key.data_limit and key.data_limit.bytes == limit
    -602        """
    -603        return await self._request(
    -604            "PUT",
    -605            f"access-keys/{key_id}/data-limit",
    -606            json={"limit": {"bytes": bytes_limit}},
    -607        )
    +589        Raises:
    +590            APIError: If key doesn't exist or limit is invalid
    +591
    +592        Examples:
    +593            >>> async def doo_something():
    +594            ...     async with AsyncOutlineClient(
    +595            ...         "https://example.com:1234/secret",
    +596            ...         "ab12cd34..."
    +597            ...     ) as client:
    +598            ...         # Set 5 GB limit
    +599            ...         limit = 5 * 1024**3  # 5 GB in bytes
    +600            ...         await client.set_access_key_data_limit(1, limit)
    +601            ...
    +602            ...         # Verify limit
    +603            ...         key = await client.get_access_key(1)
    +604            ...         assert key.data_limit and key.data_limit.bytes == limit
    +605        """
    +606        return await self._request(
    +607            "PUT",
    +608            f"access-keys/{key_id}/data-limit",
    +609            json={"limit": {"bytes": bytes_limit}},
    +610        )
     
    @@ -2023,26 +2036,26 @@
    Examples:
    async def - remove_access_key_data_limit(self, key_id: str) -> bool: + remove_access_key_data_limit(self, key_id: int) -> bool:
    -
    609    async def remove_access_key_data_limit(self, key_id: str) -> bool:
    -610        """
    -611        Remove data transfer limit from access key.
    -612
    -613        Args:
    -614            key_id: Access key ID
    +            
    612    async def remove_access_key_data_limit(self, key_id: int) -> bool:
    +613        """
    +614        Remove data transfer limit from access key.
     615
    -616        Returns:
    -617            True if successful
    +616        Args:
    +617            key_id: Access key ID
     618
    -619        Raises:
    -620            APIError: If key doesn't exist
    -621        """
    -622        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
    +619        Returns:
    +620            True if successful
    +621
    +622        Raises:
    +623            APIError: If key doesn't exist
    +624        """
    +625        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
     
    @@ -2068,6 +2081,24 @@
    Raises:
    +
    +
    + +
    + session + + + +
    + +
    627    @property
    +628    def session(self):
    +629        return self._session
    +
    + + + +
    @@ -2081,8 +2112,8 @@
    Raises:
    -
    47class OutlineError(Exception):
    -48    """Base exception for Outline client errors."""
    +            
    48class OutlineError(Exception):
    +49    """Base exception for Outline client errors."""
     
    @@ -2102,12 +2133,12 @@
    Raises:
    -
    51class APIError(OutlineError):
    -52    """Raised when API requests fail."""
    -53
    -54    def __init__(self, message: str, status_code: Optional[int] = None) -> None:
    -55        super().__init__(message)
    -56        self.status_code = status_code
    +            
    52class APIError(OutlineError):
    +53    """Raised when API requests fail."""
    +54
    +55    def __init__(self, message: str, status_code: Optional[int] = None) -> None:
    +56        super().__init__(message)
    +57        self.status_code = status_code
     
    @@ -2125,9 +2156,9 @@
    Raises:
    -
    54    def __init__(self, message: str, status_code: Optional[int] = None) -> None:
    -55        super().__init__(message)
    -56        self.status_code = status_code
    +            
    55    def __init__(self, message: str, status_code: Optional[int] = None) -> None:
    +56        super().__init__(message)
    +57        self.status_code = status_code
     
    @@ -2157,16 +2188,16 @@
    Raises:
    -
    41class AccessKey(BaseModel):
    -42    """Access key details."""
    -43
    -44    id: int
    -45    name: Optional[str] = None
    -46    password: str
    -47    port: int = Field(gt=0, lt=65536)
    -48    method: str
    -49    access_url: str = Field(alias="accessUrl")
    -50    data_limit: Optional[DataLimit] = Field(None, alias="dataLimit")
    +            
    42class AccessKey(BaseModel):
    +43    """Access key details."""
    +44
    +45    id: int
    +46    name: Optional[str] = None
    +47    password: str
    +48    port: int = Field(gt=0, lt=65536)
    +49    method: str
    +50    access_url: str = Field(alias="accessUrl")
    +51    data_limit: Optional[DataLimit] = Field(None, alias="dataLimit")
     
    @@ -2277,17 +2308,17 @@
    Raises:
    -
    118class AccessKeyCreateRequest(BaseModel):
    -119    """
    -120    Request parameters for creating an access key.
    -121    Per OpenAPI: /access-keys POST request body
    -122    """
    -123
    -124    name: Optional[str] = None
    -125    method: Optional[str] = None
    -126    password: Optional[str] = None
    -127    port: Optional[int] = Field(None, gt=0, lt=65536)
    -128    limit: Optional[DataLimit] = None
    +            
    119class AccessKeyCreateRequest(BaseModel):
    +120    """
    +121    Request parameters for creating an access key.
    +122    Per OpenAPI: /access-keys POST request body
    +123    """
    +124
    +125    name: Optional[str] = None
    +126    method: Optional[str] = None
    +127    password: Optional[str] = None
    +128    port: Optional[int] = Field(None, gt=0, lt=65536)
    +129    limit: Optional[DataLimit] = None
     
    @@ -2377,10 +2408,10 @@
    Raises:
    -
    53class AccessKeyList(BaseModel):
    -54    """List of access keys."""
    -55
    -56    access_keys: list[AccessKey] = Field(alias="accessKeys")
    +            
    54class AccessKeyList(BaseModel):
    +55    """List of access keys."""
    +56
    +57    access_keys: list[AccessKey] = Field(alias="accessKeys")
     
    @@ -2425,16 +2456,16 @@
    Raises:
    -
    29class DataLimit(BaseModel):
    -30    """Data transfer limit configuration."""
    -31
    -32    bytes: int = Field(gt=0)
    -33
    -34    @field_validator("bytes")
    -35    def validate_bytes(cls, v: int) -> int:
    -36        if v < 0:
    -37            raise ValueError("bytes must be positive")
    -38        return v
    +            
    30class DataLimit(BaseModel):
    +31    """Data transfer limit configuration."""
    +32
    +33    bytes: int = Field(gt=0)
    +34
    +35    @field_validator("bytes")
    +36    def validate_bytes(cls, v: int) -> int:
    +37        if v < 0:
    +38            raise ValueError("bytes must be positive")
    +39        return v
     
    @@ -2465,11 +2496,11 @@
    Raises:
    -
    34    @field_validator("bytes")
    -35    def validate_bytes(cls, v: int) -> int:
    -36        if v < 0:
    -37            raise ValueError("bytes must be positive")
    -38        return v
    +            
    35    @field_validator("bytes")
    +36    def validate_bytes(cls, v: int) -> int:
    +37        if v < 0:
    +38            raise ValueError("bytes must be positive")
    +39        return v
     
    @@ -2502,14 +2533,14 @@
    Raises:
    -
    137class ErrorResponse(BaseModel):
    -138    """
    -139    Error response structure
    -140    Per OpenAPI: 404 and 400 responses
    -141    """
    -142
    -143    code: str
    -144    message: str
    +            
    138class ErrorResponse(BaseModel):
    +139    """
    +140    Error response structure
    +141    Per OpenAPI: 404 and 400 responses
    +142    """
    +143
    +144    code: str
    +145    message: str
     
    @@ -2566,14 +2597,14 @@
    Raises:
    -
    92class ExperimentalMetrics(BaseModel):
    -93    """
    -94    Experimental metrics data structure
    -95    Per OpenAPI: /experimental/server/metrics endpoint
    -96    """
    -97
    -98    server: list[ServerMetric]
    -99    access_keys: list[AccessKeyMetric] = Field(alias="accessKeys")
    +            
     93class ExperimentalMetrics(BaseModel):
    + 94    """
    + 95    Experimental metrics data structure
    + 96    Per OpenAPI: /experimental/server/metrics endpoint
    + 97    """
    + 98
    + 99    server: list[ServerMetric]
    +100    access_keys: list[AccessKeyMetric] = Field(alias="accessKeys")
     
    @@ -2630,12 +2661,12 @@
    Raises:
    -
    21class MetricsPeriod(str, Enum):
    -22    """Time periods for metrics collection."""
    -23
    -24    DAILY = "daily"
    -25    WEEKLY = "weekly"
    -26    MONTHLY = "monthly"
    +            
    22class MetricsPeriod(str, Enum):
    +23    """Time periods for metrics collection."""
    +24
    +25    DAILY = "daily"
    +26    WEEKLY = "weekly"
    +27    MONTHLY = "monthly"
     
    @@ -2691,10 +2722,10 @@
    Raises:
    -
    131class MetricsStatusResponse(BaseModel):
    -132    """Response for /metrics/enabled endpoint"""
    -133
    -134    metrics_enabled: bool = Field(alias="metricsEnabled")
    +            
    132class MetricsStatusResponse(BaseModel):
    +133    """Response for /metrics/enabled endpoint"""
    +134
    +135    metrics_enabled: bool = Field(alias="metricsEnabled")
     
    @@ -2739,20 +2770,20 @@
    Raises:
    -
    102class Server(BaseModel):
    -103    """
    -104    Server information.
    -105    Per OpenAPI: /server endpoint schema
    -106    """
    -107
    -108    name: str
    -109    server_id: str = Field(alias="serverId")
    -110    metrics_enabled: bool = Field(alias="metricsEnabled")
    -111    created_timestamp_ms: int = Field(alias="createdTimestampMs")
    -112    version: str
    -113    port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536)
    -114    hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys")
    -115    access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit")
    +            
    103class Server(BaseModel):
    +104    """
    +105    Server information.
    +106    Per OpenAPI: /server endpoint schema
    +107    """
    +108
    +109    name: str
    +110    server_id: str = Field(alias="serverId")
    +111    metrics_enabled: bool = Field(alias="metricsEnabled")
    +112    created_timestamp_ms: int = Field(alias="createdTimestampMs")
    +113    version: str
    +114    port_for_new_access_keys: int = Field(alias="portForNewAccessKeys", gt=0, lt=65536)
    +115    hostname_for_access_keys: Optional[str] = Field(None, alias="hostnameForAccessKeys")
    +116    access_key_data_limit: Optional[DataLimit] = Field(None, alias="accessKeyDataLimit")
     
    @@ -2875,15 +2906,15 @@
    Raises:
    -
    59class ServerMetrics(BaseModel):
    -60    """
    -61    Server metrics data for data transferred per access key
    -62    Per OpenAPI: /metrics/transfer endpoint
    -63    """
    -64
    -65    bytes_transferred_by_user_id: dict[str, int] = Field(
    -66        alias="bytesTransferredByUserId"
    -67    )
    +            
    60class ServerMetrics(BaseModel):
    +61    """
    +62    Server metrics data for data transferred per access key
    +63    Per OpenAPI: /metrics/transfer endpoint
    +64    """
    +65
    +66    bytes_transferred_by_user_id: dict[str, int] = Field(
    +67        alias="bytesTransferredByUserId"
    +68    )
     
    diff --git a/docs/search.js b/docs/search.js index 3e0bc1d..88a45c7 100644 --- a/docs/search.js +++ b/docs/search.js @@ -1,6 +1,6 @@ window.pdocSearch = (function(){ /** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();oPyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    \n\n

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru\nAll rights reserved.

    \n\n

    This software is licensed under the MIT License.

    \n\n
    You can find the full license text at:
    \n\n
    \n

    https://opensource.org/licenses/MIT

    \n
    \n\n
    Source code repository:
    \n\n
    \n

    https://github.com/orenlab/pyoutlineapi

    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

    Asynchronous client for the Outline VPN Server API.

    \n\n
    Arguments:
    \n\n
      \n
    • api_url: Base URL for the Outline server API
    • \n
    • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    • \n
    • json_format: Return raw JSON instead of Pydantic models
    • \n
    • timeout: Request timeout in seconds
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server_info = await client.get_server_info()\n
    \n
    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\tjson_format: bool = True,\ttimeout: float = 30.0)"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_info", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_info", "kind": "function", "doc": "

    Get server information.

    \n\n
    Returns:
    \n\n
    \n

    Server information including name, ID, and configuration.

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server = await client.get_server_info()\n...         print(f"Server {server.name} running version {server.version}")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.Server]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_server", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_server", "kind": "function", "doc": "

    Rename the server.

    \n\n
    Arguments:
    \n\n
      \n
    • name: New server name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...     success = await client.rename_server("My VPN Server")\n...     if success:\n...         print("Server renamed successfully")\n
    \n
    \n
    \n", "signature": "(self, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_hostname", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_hostname", "kind": "function", "doc": "

    Set server hostname for access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • hostname: New hostname or IP address
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If hostname is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_hostname("vpn.example.com")\n...         # Or use IP address\n...         await client.set_hostname("203.0.113.1")\n
    \n
    \n
    \n", "signature": "(self, hostname: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_default_port", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_default_port", "kind": "function", "doc": "

    Set default port for new access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • port: Port number (1025-65535)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If port is invalid or in use
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_default_port(8388)\n
    \n
    \n
    \n", "signature": "(self, port: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_metrics_status", "kind": "function", "doc": "

    Get whether metrics collection is enabled.

    \n\n
    Returns:
    \n\n
    \n

    Current metrics collection status

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.get_metrics_status():\n...             print("Metrics collection is enabled")\n
    \n
    \n
    \n", "signature": "(self) -> dict[str, typing.Any] | pydantic.main.BaseModel:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_metrics_status", "kind": "function", "doc": "

    Enable or disable metrics collection.

    \n\n
    Arguments:
    \n\n
      \n
    • enabled: Whether to enable metrics
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Enable metrics\n...         await client.set_metrics_status(True)\n...         # Check new status\n...         is_enabled = await client.get_metrics_status()\n
    \n
    \n
    \n", "signature": "(self, enabled: bool) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_transfer_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_transfer_metrics", "kind": "function", "doc": "

    Get transfer metrics for specified period.

    \n\n
    Arguments:
    \n\n
      \n
    • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Transfer metrics data for each access key

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Get monthly metrics\n...         metrics = await client.get_transfer_metrics()\n...         # Or get daily metrics\n...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)\n...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():\n...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")\n
    \n
    \n
    \n", "signature": "(\tself,\tperiod: pyoutlineapi.models.MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], pyoutlineapi.models.ServerMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key", "kind": "function", "doc": "

    Create a new access key.

    \n\n
    Arguments:
    \n\n
      \n
    • name: Optional key name
    • \n
    • password: Optional password
    • \n
    • port: Optional port number (1-65535)
    • \n
    • method: Optional encryption method
    • \n
    • limit: Optional data transfer limit
    • \n
    \n\n
    Returns:
    \n\n
    \n

    New access key details

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Create basic key\n...         key = await client.create_access_key(name="User 1")\n...\n...         # Create key with data limit\n...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB\n...         key = await client.create_access_key(\n...             name="Limited User",\n...             port=8388,\n...             limit=_limit\n...         )\n...         print(f"Created key: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_keys", "kind": "function", "doc": "

    Get all access keys.

    \n\n
    Returns:
    \n\n
    \n

    List of all access keys

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         keys = await client.get_access_keys()\n...         for key in keys.access_keys:\n...             print(f"Key {key.id}: {key.name or 'unnamed'}")\n...             if key.data_limit:\n...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.AccessKeyList]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_key", "kind": "function", "doc": "

    Get specific access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Access key details

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.get_access_key(1)\n...         print(f"Port: {key.port}")\n...         print(f"URL: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\tkey_id: int) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_access_key", "kind": "function", "doc": "

    Rename access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • name: New name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Rename key\n...         await client.rename_access_key(1, "Alice")\n...\n...         # Verify new name\n...         key = await client.get_access_key(1)\n...         assert key.name == "Alice"\n
    \n
    \n
    \n", "signature": "(self, key_id: int, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.delete_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.delete_access_key", "kind": "function", "doc": "

    Delete access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.delete_access_key(1):\n...             print("Key deleted")\n
    \n
    \n
    \n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_access_key_data_limit", "kind": "function", "doc": "

    Set data transfer limit for access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • bytes_limit: Limit in bytes (must be positive)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist or limit is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 5 GB limit\n...         limit = 5 * 1024**3  # 5 GB in bytes\n...         await client.set_access_key_data_limit(1, limit)\n...\n...         # Verify limit\n...         key = await client.get_access_key(1)\n...         assert key.data_limit and key.data_limit.bytes == limit\n
    \n
    \n
    \n", "signature": "(self, key_id: int, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_access_key_data_limit", "kind": "function", "doc": "

    Remove data transfer limit from access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n", "signature": "(self, key_id: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

    Base exception for Outline client errors.

    \n", "bases": "builtins.Exception"}, {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

    Raised when API requests fail.

    \n", "bases": "pyoutlineapi.client.OutlineError"}, {"fullname": "pyoutlineapi.APIError.__init__", "modulename": "pyoutlineapi", "qualname": "APIError.__init__", "kind": "function", "doc": "

    \n", "signature": "(message: str, status_code: Optional[int] = None)"}, {"fullname": "pyoutlineapi.APIError.status_code", "modulename": "pyoutlineapi", "qualname": "APIError.status_code", "kind": "variable", "doc": "

    \n"}, {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

    Access key details.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKey.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKey.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

    Request parameters for creating an access key.\nPer OpenAPI: /access-keys POST request body

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[int]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

    List of access keys.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKey]"}, {"fullname": "pyoutlineapi.AccessKeyList.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

    Data transfer limit configuration.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.DataLimit.validate_bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.validate_bytes", "kind": "function", "doc": "

    \n", "signature": "(cls, v: int) -> int:", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

    Error response structure\nPer OpenAPI: 404 and 400 responses

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.model_config", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

    Experimental metrics data structure\nPer OpenAPI: /experimental/server/metrics endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.ServerMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsPeriod", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod", "kind": "class", "doc": "

    Time periods for metrics collection.

    \n", "bases": "builtins.str, enum.Enum"}, {"fullname": "pyoutlineapi.MetricsPeriod.DAILY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.DAILY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.DAILY: 'daily'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.WEEKLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.WEEKLY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.WEEKLY: 'weekly'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.MONTHLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.MONTHLY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.MONTHLY: 'monthly'>"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

    Response for /metrics/enabled endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

    Server information.\nPer OpenAPI: /server endpoint schema

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.Server.model_config", "modulename": "pyoutlineapi", "qualname": "Server.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

    Server metrics data for data transferred per access key\nPer OpenAPI: /metrics/transfer endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, int]"}, {"fullname": "pyoutlineapi.ServerMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}]; + /** pdoc search index */const docs = [{"fullname": "pyoutlineapi", "modulename": "pyoutlineapi", "kind": "module", "doc": "

    PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    \n\n

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru\nAll rights reserved.

    \n\n

    This software is licensed under the MIT License.

    \n\n
    You can find the full license text at:
    \n\n
    \n

    https://opensource.org/licenses/MIT

    \n
    \n\n
    Source code repository:
    \n\n
    \n

    https://github.com/orenlab/pyoutlineapi

    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

    Asynchronous client for the Outline VPN Server API.

    \n\n
    Arguments:
    \n\n
      \n
    • api_url: Base URL for the Outline server API
    • \n
    • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    • \n
    • json_format: Return raw JSON instead of Pydantic models
    • \n
    • timeout: Request timeout in seconds
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server_info = await client.get_server_info()\n
    \n
    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\tjson_format: bool = True,\ttimeout: float = 30.0)"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_info", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_info", "kind": "function", "doc": "

    Get server information.

    \n\n
    Returns:
    \n\n
    \n

    Server information including name, ID, and configuration.

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server = await client.get_server_info()\n...         print(f"Server {server.name} running version {server.version}")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.Server]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_server", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_server", "kind": "function", "doc": "

    Rename the server.

    \n\n
    Arguments:
    \n\n
      \n
    • name: New server name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...     success = await client.rename_server("My VPN Server")\n...     if success:\n...         print("Server renamed successfully")\n
    \n
    \n
    \n", "signature": "(self, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_hostname", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_hostname", "kind": "function", "doc": "

    Set server hostname for access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • hostname: New hostname or IP address
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If hostname is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_hostname("vpn.example.com")\n...         # Or use IP address\n...         await client.set_hostname("203.0.113.1")\n
    \n
    \n
    \n", "signature": "(self, hostname: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_default_port", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_default_port", "kind": "function", "doc": "

    Set default port for new access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • port: Port number (1025-65535)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If port is invalid or in use
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_default_port(8388)\n
    \n
    \n
    \n", "signature": "(self, port: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_metrics_status", "kind": "function", "doc": "

    Get whether metrics collection is enabled.

    \n\n
    Returns:
    \n\n
    \n

    Current metrics collection status

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.get_metrics_status():\n...             print("Metrics collection is enabled")\n
    \n
    \n
    \n", "signature": "(self) -> dict[str, typing.Any] | pydantic.main.BaseModel:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_metrics_status", "kind": "function", "doc": "

    Enable or disable metrics collection.

    \n\n
    Arguments:
    \n\n
      \n
    • enabled: Whether to enable metrics
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Enable metrics\n...         await client.set_metrics_status(True)\n...         # Check new status\n...         is_enabled = await client.get_metrics_status()\n
    \n
    \n
    \n", "signature": "(self, enabled: bool) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_transfer_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_transfer_metrics", "kind": "function", "doc": "

    Get transfer metrics for specified period.

    \n\n
    Arguments:
    \n\n
      \n
    • period: Time period for metrics (DAILY, WEEKLY, or MONTHLY)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Transfer metrics data for each access key

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Get monthly metrics\n...         metrics = await client.get_transfer_metrics()\n...         # Or get daily metrics\n...         daily = await client.get_transfer_metrics(MetricsPeriod.DAILY)\n...         for user_id, bytes_transferred in daily.bytes_transferred_by_user_id.items():\n...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")\n
    \n
    \n
    \n", "signature": "(\tself,\tperiod: pyoutlineapi.models.MetricsPeriod = <MetricsPeriod.MONTHLY: 'monthly'>) -> Union[dict[str, Any], pyoutlineapi.models.ServerMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key", "kind": "function", "doc": "

    Create a new access key.

    \n\n
    Arguments:
    \n\n
      \n
    • name: Optional key name
    • \n
    • password: Optional password
    • \n
    • port: Optional port number (1-65535)
    • \n
    • method: Optional encryption method
    • \n
    • limit: Optional data transfer limit
    • \n
    \n\n
    Returns:
    \n\n
    \n

    New access key details

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Create basic key\n...         key = await client.create_access_key(name="User 1")\n...\n...         # Create key with data limit\n...         _limit = DataLimit(bytes=5 * 1024**3)  # 5 GB\n...         key = await client.create_access_key(\n...             name="Limited User",\n...             port=8388,\n...             limit=_limit\n...         )\n...         print(f"Created key: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_keys", "kind": "function", "doc": "

    Get all access keys.

    \n\n
    Returns:
    \n\n
    \n

    List of all access keys

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         keys = await client.get_access_keys()\n...         for key in keys.access_keys:\n...             print(f"Key {key.id}: {key.name or 'unnamed'}")\n...             if key.data_limit:\n...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.AccessKeyList]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_key", "kind": "function", "doc": "

    Get specific access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Access key details

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.get_access_key(1)\n...         print(f"Port: {key.port}")\n...         print(f"URL: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\tkey_id: int) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_access_key", "kind": "function", "doc": "

    Rename access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • name: New name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Rename key\n...         await client.rename_access_key(1, "Alice")\n...\n...         # Verify new name\n...         key = await client.get_access_key(1)\n...         assert key.name == "Alice"\n
    \n
    \n
    \n", "signature": "(self, key_id: int, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.delete_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.delete_access_key", "kind": "function", "doc": "

    Delete access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.delete_access_key(1):\n...             print("Key deleted")\n
    \n
    \n
    \n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_access_key_data_limit", "kind": "function", "doc": "

    Set data transfer limit for access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • bytes_limit: Limit in bytes (must be positive)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist or limit is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def doo_something():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 5 GB limit\n...         limit = 5 * 1024**3  # 5 GB in bytes\n...         await client.set_access_key_data_limit(1, limit)\n...\n...         # Verify limit\n...         key = await client.get_access_key(1)\n...         assert key.data_limit and key.data_limit.bytes == limit\n
    \n
    \n
    \n", "signature": "(self, key_id: int, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_access_key_data_limit", "kind": "function", "doc": "

    Remove data transfer limit from access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n", "signature": "(self, key_id: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.session", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.session", "kind": "variable", "doc": "

    \n"}, {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

    Base exception for Outline client errors.

    \n", "bases": "builtins.Exception"}, {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

    Raised when API requests fail.

    \n", "bases": "pyoutlineapi.client.OutlineError"}, {"fullname": "pyoutlineapi.APIError.__init__", "modulename": "pyoutlineapi", "qualname": "APIError.__init__", "kind": "function", "doc": "

    \n", "signature": "(message: str, status_code: Optional[int] = None)"}, {"fullname": "pyoutlineapi.APIError.status_code", "modulename": "pyoutlineapi", "qualname": "APIError.status_code", "kind": "variable", "doc": "

    \n"}, {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

    Access key details.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKey.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKey.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

    Request parameters for creating an access key.\nPer OpenAPI: /access-keys POST request body

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[int]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

    List of access keys.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKey]"}, {"fullname": "pyoutlineapi.AccessKeyList.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

    Data transfer limit configuration.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.DataLimit.validate_bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.validate_bytes", "kind": "function", "doc": "

    \n", "signature": "(cls, v: int) -> int:", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

    Error response structure\nPer OpenAPI: 404 and 400 responses

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.model_config", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

    Experimental metrics data structure\nPer OpenAPI: /experimental/server/metrics endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.ServerMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsPeriod", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod", "kind": "class", "doc": "

    Time periods for metrics collection.

    \n", "bases": "builtins.str, enum.Enum"}, {"fullname": "pyoutlineapi.MetricsPeriod.DAILY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.DAILY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.DAILY: 'daily'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.WEEKLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.WEEKLY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.WEEKLY: 'weekly'>"}, {"fullname": "pyoutlineapi.MetricsPeriod.MONTHLY", "modulename": "pyoutlineapi", "qualname": "MetricsPeriod.MONTHLY", "kind": "variable", "doc": "

    \n", "default_value": "<MetricsPeriod.MONTHLY: 'monthly'>"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

    Response for /metrics/enabled endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

    Server information.\nPer OpenAPI: /server endpoint schema

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.Server.model_config", "modulename": "pyoutlineapi", "qualname": "Server.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

    Server metrics data for data transferred per access key\nPer OpenAPI: /metrics/transfer endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, int]"}, {"fullname": "pyoutlineapi.ServerMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}]; // mirrored in build-search-index.js (part 1) // Also split on html tags. this is a cheap heuristic, but good enough. diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index bf86c4e..a9971fe 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -16,7 +16,17 @@ import binascii from functools import wraps -from typing import Any, Literal, TypeAlias, Union, overload, Optional, ParamSpec, TypeVar, Callable +from typing import ( + Any, + Literal, + TypeAlias, + Union, + overload, + Optional, + ParamSpec, + TypeVar, + Callable, +) from urllib.parse import urlparse import aiohttp @@ -88,12 +98,12 @@ class AsyncOutlineClient: """ def __init__( - self, - api_url: str, - cert_sha256: str, - *, - json_format: bool = True, - timeout: float = 30.0, + self, + api_url: str, + cert_sha256: str, + *, + json_format: bool = True, + timeout: float = 30.0, ) -> None: self._api_url = api_url.rstrip("/") self._cert_sha256 = cert_sha256 @@ -119,31 +129,28 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: @overload async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: Literal[True], - ) -> JsonDict: - ... + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[True], + ) -> JsonDict: ... @overload async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: Literal[False], - ) -> BaseModel: - ... + self, + response: ClientResponse, + model: type[BaseModel], + json_format: Literal[False], + ) -> BaseModel: ... @overload async def _parse_response( - self, response: ClientResponse, model: type[BaseModel], json_format: bool - ) -> Union[JsonDict, BaseModel]: - ... + self, response: ClientResponse, model: type[BaseModel], json_format: bool + ) -> Union[JsonDict, BaseModel]: ... @ensure_context async def _parse_response( - self, response: ClientResponse, model: type[BaseModel], json_format: bool = True + self, response: ClientResponse, model: type[BaseModel], json_format: bool = True ) -> ResponseType: """ Parse and validate API response data. @@ -182,22 +189,22 @@ async def _handle_error_response(response: ClientResponse) -> None: @ensure_context async def _request( - self, - method: str, - endpoint: str, - *, - json: Any = None, - params: Optional[dict[str, Any]] = None, + self, + method: str, + endpoint: str, + *, + json: Any = None, + params: Optional[dict[str, Any]] = None, ) -> Any: """Make an API request.""" url = self._build_url(endpoint) async with self._session.request( - method, - url, - json=json, - params=params, - raise_for_status=False, + method, + url, + json=json, + params=params, + raise_for_status=False, ) as response: if response.status >= 400: await self._handle_error_response(response) @@ -330,6 +337,9 @@ async def set_default_port(self, port: int) -> bool: ... await client.set_default_port(8388) """ + if port < 1025 or port > 65535: + raise ValueError("Privileged ports are not allowed. Use range: 1025-65535") + return await self._request( "PUT", "server/port-for-new-access-keys", json={"port": port} ) @@ -382,7 +392,7 @@ async def set_metrics_status(self, enabled: bool) -> bool: ) async def get_transfer_metrics( - self, period: MetricsPeriod = MetricsPeriod.MONTHLY + self, period: MetricsPeriod = MetricsPeriod.MONTHLY ) -> Union[JsonDict, ServerMetrics]: """ Get transfer metrics for specified period. @@ -414,13 +424,13 @@ async def get_transfer_metrics( ) async def create_access_key( - self, - *, - name: Optional[str] = None, - password: Optional[str] = None, - port: Optional[int] = None, - method: Optional[str] = None, - limit: Optional[DataLimit] = None, + self, + *, + name: Optional[str] = None, + password: Optional[str] = None, + port: Optional[int] = None, + method: Optional[str] = None, + limit: Optional[DataLimit] = None, ) -> Union[JsonDict, AccessKey]: """ Create a new access key. @@ -605,7 +615,7 @@ async def set_access_key_data_limit(self, key_id: int, bytes_limit: int) -> bool json={"limit": {"bytes": bytes_limit}}, ) - async def remove_access_key_data_limit(self, key_id: str) -> bool: + async def remove_access_key_data_limit(self, key_id: int) -> bool: """ Remove data transfer limit from access key. From b4d9f1092c00ab5a69a6a20a92aed42ae14f2e6e Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 10 Jan 2025 21:27:01 +0500 Subject: [PATCH 24/24] docs: update codecov badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d929b49..b5c6af6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ models. [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![tests](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml/badge.svg)](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml) -[![codecov](https://codecov.io/gh/orenlab/pyoutlineapi/branch/development/graph/badge.svg?token=D0MPKCKFJQ)](https://codecov.io/gh/orenlab/pyoutlineapi) +[![codecov](https://codecov.io/gh/orenlab/pyoutlineapi/branch/main/graph/badge.svg?token=D0MPKCKFJQ)](https://codecov.io/gh/orenlab/pyoutlineapi) ![PyPI - Downloads](https://img.shields.io/pypi/dm/pyoutlineapi) ## Features