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
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml)
+
## 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.
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](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
+
+
+
+
+ View Source
+
+ 1 from .client import AsyncOutlineClient , OutlineError , APIError
+ 2 from .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 :
+
+ View Source
+
+
+
+ 37 class 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 )
+
+ View Source
+
+
+
+
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 ] :
+
+
View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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:
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 ()
+
+
+
+
+
+
+
+
+
+
+
+
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 ] :
+
+
View Source
+
+
+
+
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 ] :
+
+
View Source
+
+
+
+
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 ] :
+
+
View Source
+
+
+
+
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:
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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:
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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 :
+
+ View Source
+
+
+
+
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:
+
+
+
+
Returns:
+
+
+ True if successful
+
+
+
Raises:
+
+
+APIError: If key doesn't exist
+
+
+
+
+
+
+
+
+
+
+ class
+ OutlineError (builtins.Exception ):
+
+ View Source
+
+
+
+ 25 class OutlineError ( Exception ):
+26 """Base exception for Outline client errors."""
+
+
+
+ Base exception for Outline client errors.
+
+
+
+
+
+
+
+
+
+ class
+ AccessKey (pydantic.main.BaseModel ):
+
+ View Source
+
+
+
+ 28 class 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" )
+
+
+
+
+
+
+
+
+ id : int
+
+
+
+
+
+
+
+
+
+
+ name : Optional[str]
+
+
+
+
+
+
+
+
+
+
+ password : str
+
+
+
+
+
+
+
+
+
+
+ port : int
+
+
+
+
+
+
+
+
+
+
+ method : str
+
+
+
+
+
+
+
+
+
+
+ access_url : str
+
+
+
+
+
+
+
+
+
+
+
+ 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 ):
+
+ View Source
+
+
+
+ 105 class 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]
+
+
+
+
+
+
+
+
+
+
+
+ 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 ):
+
+ View Source
+
+
+
+ 40 class AccessKeyList ( BaseModel ):
+41 """List of access keys."""
+42
+43 access_keys : list [ AccessKey ] = Field ( alias = "accessKeys" )
+
+
+
+
+
+
+
+
+
+ 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 ):
+
+ View Source
+
+
+
+ 16 class 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 :
+
+
View Source
+
+
+
+
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 ):
+
+ View Source
+
+
+
+ 124 class 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 ):
+
+ View Source
+
+
+
+ 79 class 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 ):
+
+ View Source
+
+
+
+ 8 class MetricsPeriod ( str , Enum ):
+ 9 """Time periods for metrics collection."""
+10
+11 DAILY = "daily"
+12 WEEKLY = "weekly"
+13 MONTHLY = "monthly"
+
+
+
+ Time periods for metrics collection.
+
+
+
+
+
+
+
+
+
+
+
+ class
+ MetricsStatusResponse (pydantic.main.BaseModel ):
+
+ View Source
+
+
+
+ 118 class 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 ):
+
+ View Source
+
+
+
+ 89 class 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 ):
+
+ View Source
+
+
+
+ 46 class 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\nArguments: \n\n\napi_url: Base URL for the Outline server API \ncert_sha256: SHA-256 fingerprint of the server's TLS certificate \njson_format: Return raw JSON instead of Pydantic models \ntimeout: Request timeout in seconds \n \n\nExamples: \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\nReturns: \n\n\n Server information including name, ID, and configuration.
\n \n\nExamples: \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\nArguments: \n\n\nname: New server name \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nhostname: New hostname or IP address \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If hostname is invalid \n \n\nExamples: \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\nArguments: \n\n\nport: Port number (1025-65535) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If port is invalid or in use \n \n\nExamples: \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\nReturns: \n\n\n Current metrics collection status
\n \n\nExamples: \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\nArguments: \n\n\nenabled: Whether to enable metrics \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nperiod: Time period for metrics (DAILY, WEEKLY, or MONTHLY) \n \n\nReturns: \n\n\n Transfer metrics data for each access key
\n \n\nExamples: \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\nArguments: \n\n\nname: Optional key name \npassword: Optional password \nport: Optional port number (1-65535) \nmethod: Optional encryption method \nlimit: Optional data transfer limit \n \n\nReturns: \n\n\n New access key details
\n \n\nExamples: \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\nReturns: \n\n\n List of all access keys
\n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n Access key details
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nname: New name \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nbytes_limit: Limit in bytes (must be positive) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist or limit is invalid \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: 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
-
+
+
View Source
- 1 from .client import AsyncOutlineClient , OutlineError , APIError
- 2 from .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 """
+ 2 PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.
+ 3
+ 4 Copyright (c) 2025 Denis Rozhnovskiy <pytelemonbot@mail.ru>
+ 5 All rights reserved.
+ 6
+ 7 This software is licensed under the MIT License.
+ 8 You can find the full license text at:
+ 9 https://opensource.org/licenses/MIT
+10
+11 Source code repository:
+12 https://github.com/orenlab/pyoutlineapi
+13 """
+14 import sys
+15 from 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 ]
+17 if sys . version_info < ( 3 , 10 ):
+18 raise RuntimeError ( "PyOutlineAPI requires Python 3.10 or higher" )
+19
+20 from .client import AsyncOutlineClient , OutlineError , APIError
+21
+22 if 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
+41 PUBLIC_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
+60 from .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 @@
- 37 class 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.
+ 71 class 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:
- 25 class OutlineError ( Exception ):
-26 """Base exception for Outline client errors."""
+ 47 class OutlineError ( Exception ):
+48 """Base exception for Outline client errors."""
@@ -2046,8 +2102,12 @@ Raises:
- 29 class APIError ( OutlineError ):
-30 """Raised when API requests fail."""
+ 51 class 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 )
+
+ View Source
+
+
+
+
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:
- 28 class 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" )
+ 41 class 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:
- 105 class 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
+ 118 class 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:
- 40 class AccessKeyList ( BaseModel ):
-41 """List of access keys."""
-42
-43 access_keys : list [ AccessKey ] = Field ( alias = "accessKeys" )
+ 53 class AccessKeyList ( BaseModel ):
+54 """List of access keys."""
+55
+56 access_keys : list [ AccessKey ] = Field ( alias = "accessKeys" )
@@ -2335,16 +2425,16 @@ Raises:
- 16 class 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
+ 29 class 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:
- 124 class ErrorResponse ( BaseModel ):
-125 """
-126 Error response structure
-127 Per OpenAPI: 404 and 400 responses
-128 """
-129
-130 code : str
-131 message : str
+ 137 class 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:
- 79 class 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" )
+ 92 class 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:
- 8 class MetricsPeriod ( str , Enum ):
- 9 """Time periods for metrics collection."""
-10
-11 DAILY = "daily"
-12 WEEKLY = "weekly"
-13 MONTHLY = "monthly"
+ 21 class MetricsPeriod ( str , Enum ):
+22 """Time periods for metrics collection."""
+23
+24 DAILY = "daily"
+25 WEEKLY = "weekly"
+26 MONTHLY = "monthly"
@@ -2601,10 +2691,10 @@ Raises:
- 118 class MetricsStatusResponse ( BaseModel ):
-119 """Response for /metrics/enabled endpoint"""
-120
-121 metrics_enabled : bool = Field ( alias = "metricsEnabled" )
+ 131 class MetricsStatusResponse ( BaseModel ):
+132 """Response for /metrics/enabled endpoint"""
+133
+134 metrics_enabled : bool = Field ( alias = "metricsEnabled" )
@@ -2649,20 +2739,20 @@ Raises:
- 89 class 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" )
+ 102 class 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:
- 46 class 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 )
+ 59 class 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\nArguments: \n\n\napi_url: Base URL for the Outline server API \ncert_sha256: SHA-256 fingerprint of the server's TLS certificate \njson_format: Return raw JSON instead of Pydantic models \ntimeout: Request timeout in seconds \n \n\nExamples: \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\nReturns: \n\n\n Server information including name, ID, and configuration.
\n \n\nExamples: \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\nArguments: \n\n\nname: New server name \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nhostname: New hostname or IP address \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If hostname is invalid \n \n\nExamples: \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\nArguments: \n\n\nport: Port number (1025-65535) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If port is invalid or in use \n \n\nExamples: \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\nReturns: \n\n\n Current metrics collection status
\n \n\nExamples: \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\nArguments: \n\n\nenabled: Whether to enable metrics \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nperiod: Time period for metrics (DAILY, WEEKLY, or MONTHLY) \n \n\nReturns: \n\n\n Transfer metrics data for each access key
\n \n\nExamples: \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\nArguments: \n\n\nname: Optional key name \npassword: Optional password \nport: Optional port number (1-65535) \nmethod: Optional encryption method \nlimit: Optional data transfer limit \n \n\nReturns: \n\n\n New access key details
\n \n\nExamples: \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\nReturns: \n\n\n List of all access keys
\n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n Access key details
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nname: New name \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nbytes_limit: Limit in bytes (must be positive) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist or limit is invalid \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: 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\nCopyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru \nAll rights reserved.
\n\nThis software is licensed under the MIT License.
\n\nYou can find the full license text at: \n\n\n https://opensource.org/licenses/MIT
\n \n\nSource 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\nArguments: \n\n\napi_url: Base URL for the Outline server API \ncert_sha256: SHA-256 fingerprint of the server's TLS certificate \njson_format: Return raw JSON instead of Pydantic models \ntimeout: Request timeout in seconds \n \n\nExamples: \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\nReturns: \n\n\n Server information including name, ID, and configuration.
\n \n\nExamples: \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\nArguments: \n\n\nname: New server name \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nhostname: New hostname or IP address \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If hostname is invalid \n \n\nExamples: \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\nArguments: \n\n\nport: Port number (1025-65535) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If port is invalid or in use \n \n\nExamples: \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\nReturns: \n\n\n Current metrics collection status
\n \n\nExamples: \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\nArguments: \n\n\nenabled: Whether to enable metrics \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nperiod: Time period for metrics (DAILY, WEEKLY, or MONTHLY) \n \n\nReturns: \n\n\n Transfer metrics data for each access key
\n \n\nExamples: \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\nArguments: \n\n\nname: Optional key name \npassword: Optional password \nport: Optional port number (1-65535) \nmethod: Optional encryption method \nlimit: Optional data transfer limit \n \n\nReturns: \n\n\n New access key details
\n \n\nExamples: \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\nReturns: \n\n\n List of all access keys
\n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n Access key details
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nname: New name \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nbytes_limit: Limit in bytes (must be positive) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist or limit is invalid \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: 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.
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml)
+[](https://codecov.io/gh/orenlab/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:
11 Source code repository:
12 https://github.com/orenlab/pyoutlineapi
13 """
-14 import sys
-15 from typing import TYPE_CHECKING
-16
-17 if sys . version_info < ( 3 , 10 ):
-18 raise RuntimeError ( "PyOutlineAPI requires Python 3.10 or higher" )
-19
-20 from .client import AsyncOutlineClient , OutlineError , APIError
-21
-22 if 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
-41 PUBLIC_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
-60 from .models import (
-61 AccessKey ,
-62 AccessKeyCreateRequest ,
-63 AccessKeyList ,
-64 DataLimit ,
-65 ErrorResponse ,
-66 ExperimentalMetrics ,
-67 MetricsPeriod ,
-68 MetricsStatusResponse ,
-69 Server ,
-70 ServerMetrics ,
-71 )
+14
+15 import sys
+16 from typing import TYPE_CHECKING
+17
+18 if sys . version_info < ( 3 , 10 ):
+19 raise RuntimeError ( "PyOutlineAPI requires Python 3.10 or higher" )
+20
+21 from .client import AsyncOutlineClient , OutlineError , APIError
+22
+23 if 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
+42 PUBLIC_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
+61 from .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:
- 71 class 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 )
+ 72 class 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 :
View Source
- 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
+
+ View Source
+
+
+
+
627 @property
+628 def session ( self ):
+629 return self . _session
+
+
+
+
+
@@ -2081,8 +2112,8 @@ Raises:
- 47 class OutlineError ( Exception ):
-48 """Base exception for Outline client errors."""
+ 48 class OutlineError ( Exception ):
+49 """Base exception for Outline client errors."""
@@ -2102,12 +2133,12 @@ Raises:
- 51 class 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
+ 52 class 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:
- 41 class 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" )
+ 42 class 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:
- 118 class 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
+ 119 class 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:
- 53 class AccessKeyList ( BaseModel ):
-54 """List of access keys."""
-55
-56 access_keys : list [ AccessKey ] = Field ( alias = "accessKeys" )
+ 54 class AccessKeyList ( BaseModel ):
+55 """List of access keys."""
+56
+57 access_keys : list [ AccessKey ] = Field ( alias = "accessKeys" )
@@ -2425,16 +2456,16 @@ Raises:
- 29 class 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
+ 30 class 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:
- 137 class ErrorResponse ( BaseModel ):
-138 """
-139 Error response structure
-140 Per OpenAPI: 404 and 400 responses
-141 """
-142
-143 code : str
-144 message : str
+ 138 class 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:
- 92 class 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" )
+ 93 class 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:
- 21 class MetricsPeriod ( str , Enum ):
-22 """Time periods for metrics collection."""
-23
-24 DAILY = "daily"
-25 WEEKLY = "weekly"
-26 MONTHLY = "monthly"
+ 22 class MetricsPeriod ( str , Enum ):
+23 """Time periods for metrics collection."""
+24
+25 DAILY = "daily"
+26 WEEKLY = "weekly"
+27 MONTHLY = "monthly"
@@ -2691,10 +2722,10 @@ Raises:
- 131 class MetricsStatusResponse ( BaseModel ):
-132 """Response for /metrics/enabled endpoint"""
-133
-134 metrics_enabled : bool = Field ( alias = "metricsEnabled" )
+ 132 class MetricsStatusResponse ( BaseModel ):
+133 """Response for /metrics/enabled endpoint"""
+134
+135 metrics_enabled : bool = Field ( alias = "metricsEnabled" )
@@ -2739,20 +2770,20 @@ Raises:
- 102 class 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" )
+ 103 class 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:
- 59 class 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 )
+ 60 class 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\nCopyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru \nAll rights reserved.
\n\nThis software is licensed under the MIT License.
\n\nYou can find the full license text at: \n\n\n https://opensource.org/licenses/MIT
\n \n\nSource 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\nArguments: \n\n\napi_url: Base URL for the Outline server API \ncert_sha256: SHA-256 fingerprint of the server's TLS certificate \njson_format: Return raw JSON instead of Pydantic models \ntimeout: Request timeout in seconds \n \n\nExamples: \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\nReturns: \n\n\n Server information including name, ID, and configuration.
\n \n\nExamples: \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\nArguments: \n\n\nname: New server name \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nhostname: New hostname or IP address \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If hostname is invalid \n \n\nExamples: \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\nArguments: \n\n\nport: Port number (1025-65535) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If port is invalid or in use \n \n\nExamples: \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\nReturns: \n\n\n Current metrics collection status
\n \n\nExamples: \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\nArguments: \n\n\nenabled: Whether to enable metrics \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nperiod: Time period for metrics (DAILY, WEEKLY, or MONTHLY) \n \n\nReturns: \n\n\n Transfer metrics data for each access key
\n \n\nExamples: \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\nArguments: \n\n\nname: Optional key name \npassword: Optional password \nport: Optional port number (1-65535) \nmethod: Optional encryption method \nlimit: Optional data transfer limit \n \n\nReturns: \n\n\n New access key details
\n \n\nExamples: \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\nReturns: \n\n\n List of all access keys
\n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n Access key details
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nname: New name \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nbytes_limit: Limit in bytes (must be positive) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist or limit is invalid \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: 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\nCopyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru \nAll rights reserved.
\n\nThis software is licensed under the MIT License.
\n\nYou can find the full license text at: \n\n\n https://opensource.org/licenses/MIT
\n \n\nSource 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\nArguments: \n\n\napi_url: Base URL for the Outline server API \ncert_sha256: SHA-256 fingerprint of the server's TLS certificate \njson_format: Return raw JSON instead of Pydantic models \ntimeout: Request timeout in seconds \n \n\nExamples: \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\nReturns: \n\n\n Server information including name, ID, and configuration.
\n \n\nExamples: \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\nArguments: \n\n\nname: New server name \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nhostname: New hostname or IP address \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If hostname is invalid \n \n\nExamples: \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\nArguments: \n\n\nport: Port number (1025-65535) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If port is invalid or in use \n \n\nExamples: \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\nReturns: \n\n\n Current metrics collection status
\n \n\nExamples: \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\nArguments: \n\n\nenabled: Whether to enable metrics \n \n\nReturns: \n\n\n True if successful
\n \n\nExamples: \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\nArguments: \n\n\nperiod: Time period for metrics (DAILY, WEEKLY, or MONTHLY) \n \n\nReturns: \n\n\n Transfer metrics data for each access key
\n \n\nExamples: \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\nArguments: \n\n\nname: Optional key name \npassword: Optional password \nport: Optional port number (1-65535) \nmethod: Optional encryption method \nlimit: Optional data transfer limit \n \n\nReturns: \n\n\n New access key details
\n \n\nExamples: \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\nReturns: \n\n\n List of all access keys
\n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n Access key details
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nname: New name \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \nbytes_limit: Limit in bytes (must be positive) \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: If key doesn't exist or limit is invalid \n \n\nExamples: \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\nArguments: \n\n\nkey_id: Access key ID \n \n\nReturns: \n\n\n True if successful
\n \n\nRaises: \n\n\nAPIError: 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.
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi)
[](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml)
-[](https://codecov.io/gh/orenlab/pyoutlineapi)
+[](https://codecov.io/gh/orenlab/pyoutlineapi)

## Features