diff --git a/poetry.lock b/poetry.lock index 8691605a..283ef7ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# 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.2.1 and should not be changed by hand. [[package]] name = "anyio" @@ -6,6 +6,7 @@ version = "4.3.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, @@ -17,7 +18,7 @@ sniffio = ">=1.1" [package.extras] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.23)"] [[package]] @@ -26,6 +27,7 @@ version = "3.7.2" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, @@ -40,6 +42,8 @@ version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_full_version < \"3.11.3\"" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, @@ -51,6 +55,7 @@ version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, @@ -61,8 +66,8 @@ cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6) ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\""] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "babel" @@ -70,6 +75,7 @@ version = "2.14.0" description = "Internationalization utilities" optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, @@ -84,6 +90,7 @@ 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"}, @@ -128,6 +135,7 @@ version = "1.34.162" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "boto3-1.34.162-py3-none-any.whl", hash = "sha256:d6f6096bdab35a0c0deff469563b87d184a28df7689790f7fe7be98502b7c590"}, {file = "boto3-1.34.162.tar.gz", hash = "sha256:873f8f5d2f6f85f1018cbb0535b03cceddc7b655b61f66a0a56995238804f41f"}, @@ -147,6 +155,7 @@ version = "1.35.35" description = "Type annotations for boto3 1.35.35 generated with mypy-boto3-builder 8.1.2" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "boto3_stubs-1.35.35-py3-none-any.whl", hash = "sha256:f2e9131d038cf837d12e8865f36da17add93a9539cc6fbe69df3003b8dd386e9"}, {file = "boto3_stubs-1.35.35.tar.gz", hash = "sha256:08fcc63c7f72c60214668188ced405cf0ce1961c6e4cf64adfee03296cbc4c9c"}, @@ -557,6 +566,7 @@ version = "1.34.162" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, @@ -576,6 +586,7 @@ version = "1.34.57" description = "Type annotations and code completion for botocore" optional = false python-versions = ">=3.8,<4.0" +groups = ["main"] files = [ {file = "botocore_stubs-1.34.57-py3-none-any.whl", hash = "sha256:13f133ef244ebde6425cff3b41b43898af709d0fc8a749b26ddf304eff3a6570"}, {file = "botocore_stubs-1.34.57.tar.gz", hash = "sha256:2deb03f218469c5a76dfefeb2d1e9f1599914adbb5f8bd8cc0d7ad7d7a07ae33"}, @@ -593,6 +604,7 @@ version = "5.3.3" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, @@ -604,6 +616,7 @@ version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev", "docs"] files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, @@ -615,6 +628,8 @@ version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, @@ -679,6 +694,7 @@ 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" +groups = ["main", "dev", "docs"] 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"}, @@ -778,6 +794,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev", "docs"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -792,10 +809,12 @@ 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 = ["main", "dev", "docs"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "connexion" @@ -803,6 +822,7 @@ version = "3.1.0" description = "Connexion - API first applications with OpenAPI/Swagger" optional = false python-versions = "<4.0,>=3.8" +groups = ["dev"] files = [ {file = "connexion-3.1.0-py3-none-any.whl", hash = "sha256:e92b6d0412eb54b3b69f2516b93d982a06b0e23f6d5c1ab94257c55d365f63ce"}, {file = "connexion-3.1.0.tar.gz", hash = "sha256:66a44580991f53955b6e409a84fa9fa65c7ca4db52dc217b49cd35c201066083"}, @@ -833,6 +853,7 @@ version = "7.4.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, @@ -889,7 +910,7 @@ files = [ ] [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "crontab" @@ -897,6 +918,7 @@ version = "1.0.1" description = "Parse and use crontab schedules in Python" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "crontab-1.0.1.tar.gz", hash = "sha256:89477e3f93c81365e738d5ee2659509e6373bb2846de13922663e79aa74c6b91"}, ] @@ -907,6 +929,7 @@ version = "43.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, @@ -956,6 +979,7 @@ version = "5.1.1" description = "Decorators for Humans" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, @@ -967,6 +991,7 @@ version = "7.1.0" description = "A Python library for the Docker Engine API." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, @@ -989,6 +1014,7 @@ version = "1.3.2" description = "A caching front-end based on the Dogpile lock." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "dogpile.cache-1.3.2-py3-none-any.whl", hash = "sha256:c59250e23ddb4c03259c315c3b03d18b0658ec4f30ee665b39b91faf6401ef41"}, {file = "dogpile.cache-1.3.2.tar.gz", hash = "sha256:4f71dc0333ad351c9c6f704f5ba2a37bf51c6eed0437d1adf56e075959afe63b"}, @@ -999,7 +1025,7 @@ decorator = ">=4.0.0" stevedore = ">=3.0.0" [package.extras] -pifpaf = ["pifpaf (>=2.5.0)", "setuptools"] +pifpaf = ["pifpaf (>=2.5.0)", "setuptools ; python_version >= \"3.12\""] [[package]] name = "fastapi" @@ -1007,6 +1033,7 @@ version = "0.115.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631"}, {file = "fastapi-0.115.0.tar.gz", hash = "sha256:f93b4ca3529a8ebc6fc3fcf710e5efa8de3df9b41570958abf1d97d843138004"}, @@ -1027,6 +1054,7 @@ version = "1.4.0" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"}, {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"}, @@ -1041,6 +1069,7 @@ version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, @@ -1058,6 +1087,7 @@ version = "0.11.8" description = "GitHub SDK for Python" optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ {file = "githubkit-0.11.8-py3-none-any.whl", hash = "sha256:fb6cf775ef38c21d38226565219606009cecd67358e46db99c2499199ab3b94b"}, {file = "githubkit-0.11.8.tar.gz", hash = "sha256:e6114f66cd1fcf9fa1c58987366ce75b98325d5ea708a0108533ab7417701c8f"}, @@ -1083,6 +1113,7 @@ version = "2.17.1" description = "Google API client core library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google-api-core-2.17.1.tar.gz", hash = "sha256:9df18a1f87ee0df0bc4eea2770ebc4228392d8cc4066655b320e2cfccb15db95"}, {file = "google_api_core-2.17.1-py3-none-any.whl", hash = "sha256:610c5b90092c360736baccf17bd3efbcb30dd380e7a6dc28a71059edb8bd0d8e"}, @@ -1097,7 +1128,7 @@ protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4 requests = ">=2.18.0,<3.0.0.dev0" [package.extras] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -1107,6 +1138,7 @@ version = "2.28.1" description = "Google Authentication Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google-auth-2.28.1.tar.gz", hash = "sha256:34fc3046c257cedcf1622fc4b31fc2be7923d9b4d44973d481125ecc50d83885"}, {file = "google_auth-2.28.1-py2.py3-none-any.whl", hash = "sha256:25141e2d7a14bfcba945f5e9827f98092716e99482562f15306e5b026e21aa72"}, @@ -1130,6 +1162,7 @@ version = "1.19.2" description = "Google Cloud Compute API client library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "google_cloud_compute-1.19.2-py2.py3-none-any.whl", hash = "sha256:e15b03da54fef17be18a20ebd8716e12aa5e17379e63042d15e8b6a3a4f08597"}, {file = "google_cloud_compute-1.19.2.tar.gz", hash = "sha256:3fce82abe14e76faaa81234d9a7be1e634b2ed8a27f55feefcf2b146467f7fab"}, @@ -1147,6 +1180,7 @@ version = "1.62.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "googleapis-common-protos-1.62.0.tar.gz", hash = "sha256:83f0ece9f94e5672cced82f592d2a5edf527a96ed1794f0bab36d5735c996277"}, {file = "googleapis_common_protos-1.62.0-py2.py3-none-any.whl", hash = "sha256:4750113612205514f9f6aa4cb00d523a94f3e8c06c5ad2fee466387dc4875f07"}, @@ -1164,6 +1198,7 @@ version = "1.62.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "grpcio-1.62.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:136ffd79791b1eddda8d827b607a6285474ff8a1a5735c4947b58c481e5e4271"}, {file = "grpcio-1.62.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:d6a56ba703be6b6267bf19423d888600c3f574ac7c2cc5e6220af90662a4d6b0"}, @@ -1230,6 +1265,7 @@ version = "1.62.0" description = "Status proto mapping for gRPC" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "grpcio-status-1.62.0.tar.gz", hash = "sha256:0d693e9c09880daeaac060d0c3dba1ae470a43c99e5d20dfeafd62cf7e08a85d"}, {file = "grpcio_status-1.62.0-py3-none-any.whl", hash = "sha256:3baac03fcd737310e67758c4082a188107f771d32855bce203331cd4c9aa687a"}, @@ -1246,6 +1282,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -1257,6 +1294,7 @@ version = "2.3.2" description = "Python wrapper for hiredis" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "hiredis-2.3.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:742093f33d374098aa21c1696ac6e4874b52658c870513a297a89265a4d08fe5"}, {file = "hiredis-2.3.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:9e14fb70ca4f7efa924f508975199353bf653f452e4ef0a1e47549e208f943d7"}, @@ -1375,6 +1413,7 @@ version = "0.0.24" description = "Persistent cache implementation for httpx and httpcore" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "hishel-0.0.24-py3-none-any.whl", hash = "sha256:8b6e43481485e1938d78bd35c0bcb38646fe8f2e090fedb64b4dc1d6015ffe49"}, {file = "hishel-0.0.24.tar.gz", hash = "sha256:4ac494c6bfedc431e480ab85d3435d4710230b2ad6092766b6ccf82b1d7e4152"}, @@ -1386,7 +1425,7 @@ typing-extensions = ">=4.8.0" [package.extras] redis = ["redis (==5.0.1)"] -s3 = ["boto3 (>=1.15.0,<=1.15.3)", "boto3 (>=1.15.3)"] +s3 = ["boto3 (>=1.15.0,<=1.15.3) ; python_version < \"3.12\"", "boto3 (>=1.15.3) ; python_version >= \"3.12\""] sqlite = ["anysqlite (>=0.0.5)"] yaml = ["pyyaml (==6.0.1)"] @@ -1396,6 +1435,7 @@ version = "1.0.4" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, @@ -1417,6 +1457,7 @@ version = "0.6.1" description = "A collection of framework independent HTTP protocol utils." optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, @@ -1465,6 +1506,7 @@ version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, @@ -1478,7 +1520,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1489,6 +1531,7 @@ version = "6.112.4" description = "A library for property-based testing" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "hypothesis-6.112.4-py3-none-any.whl", hash = "sha256:6d3e3038968925069d1a7e7ebfa2ed0b65b22eff6800d1e88b687b3c6d2f57b5"}, {file = "hypothesis-6.112.4.tar.gz", hash = "sha256:8fe64e4a6d0862e209e3c36b42037aee9665cb839d619d9281be45345ab7d856"}, @@ -1499,7 +1542,7 @@ attrs = ">=22.2.0" sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.73)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.14)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2)"] +all = ["backports.zoneinfo (>=0.2.1) ; python_version < \"3.9\"", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.73)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.14)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] crosshair = ["crosshair-tool (>=0.0.73)", "hypothesis-crosshair (>=0.0.14)"] @@ -1513,7 +1556,7 @@ pandas = ["pandas (>=1.1)"] pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] -zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.2)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1) ; python_version < \"3.9\"", "tzdata (>=2024.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""] [[package]] name = "idna" @@ -1521,6 +1564,7 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main", "dev", "docs"] files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -1532,6 +1576,7 @@ version = "0.5.1" description = "A port of Ruby on Rails inflector to Python" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, @@ -1543,6 +1588,7 @@ 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"}, @@ -1554,6 +1600,7 @@ version = "2.1.0" description = "Simple module to parse ISO 8601 dates" optional = false python-versions = ">=3.7,<4.0" +groups = ["main"] files = [ {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, @@ -1565,6 +1612,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -1579,6 +1627,7 @@ version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev", "docs"] files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, @@ -1596,6 +1645,7 @@ version = "1.0.1" description = "JSON Matching Expressions" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, @@ -1607,6 +1657,7 @@ version = "1.33" description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +groups = ["main"] files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -1621,6 +1672,7 @@ version = "2.4" description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +groups = ["main"] files = [ {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, @@ -1632,6 +1684,7 @@ version = "4.21.1" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, @@ -1653,6 +1706,7 @@ version = "2023.12.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, @@ -1667,6 +1721,7 @@ version = "5.8.0" description = "Authentication Library for OpenStack Identity" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "keystoneauth1-5.8.0-py3-none-any.whl", hash = "sha256:e69dff80c509ab64d4de4494658d914e81f26af720828dc584ceee74ecd666d9"}, {file = "keystoneauth1-5.8.0.tar.gz", hash = "sha256:3157c212e121164de64d63e5ef7e1daad2bd3649a68de1e971b76877019ef1c4"}, @@ -1692,6 +1747,7 @@ version = "3.5.2" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, @@ -1707,6 +1763,7 @@ version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["dev", "docs"] files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, @@ -1776,6 +1833,7 @@ version = "1.3.4" description = "A deep merge function for 🐍." optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, @@ -1787,6 +1845,7 @@ version = "1.6.1" description = "Project documentation with Markdown." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, @@ -1809,7 +1868,7 @@ watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-get-deps" @@ -1817,6 +1876,7 @@ version = "0.2.0" description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, @@ -1833,6 +1893,7 @@ version = "9.5.39" description = "Documentation that simply works" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs_material-9.5.39-py3-none-any.whl", hash = "sha256:0f2f68c8db89523cb4a59705cd01b4acd62b2f71218ccb67e1e004e560410d2b"}, {file = "mkdocs_material-9.5.39.tar.gz", hash = "sha256:25faa06142afa38549d2b781d475a86fb61de93189f532b88e69bf11e5e5c3be"}, @@ -1862,6 +1923,7 @@ version = "1.3.1" description = "Extension pack for Python Markdown and MkDocs Material." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, @@ -1873,6 +1935,7 @@ version = "9.1.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"}, {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"}, @@ -1884,6 +1947,7 @@ version = "1.35.8" description = "Type annotations for boto3.EC2 1.35.8 service generated with mypy-boto3-builder 7.26.1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "mypy_boto3_ec2-1.35.8-py3-none-any.whl", hash = "sha256:b3e17ee6082a107d7d6d7ac44062264a9fb711c5d6d9e0ce16837cda26d1be7c"}, {file = "mypy_boto3_ec2-1.35.8.tar.gz", hash = "sha256:f4cdbe524ff4039668cc168e3c6f9c68048481ab33dfb0f5d892bbf2428d1ef2"}, @@ -1898,6 +1962,7 @@ 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"}, @@ -1909,6 +1974,7 @@ version = "0.11.0" description = "Portable network interface information." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "netifaces-0.11.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1"}, {file = "netifaces-0.11.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4"}, @@ -1948,6 +2014,7 @@ version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +groups = ["dev"] files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, @@ -1962,6 +2029,7 @@ version = "4.0.0" description = "An SDK for building applications to work with OpenStack" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "openstacksdk-4.0.0-py3-none-any.whl", hash = "sha256:93e472a47d50260162d14b235e68e04a7929776594d366a4f5318d15ab8582ef"}, {file = "openstacksdk-4.0.0.tar.gz", hash = "sha256:e7860dd96b7053130923c11d571d25802b968f1e3138845cb7cac7c6b333bf4b"}, @@ -1988,6 +2056,7 @@ version = "1.7.0" description = "Python library for consuming OpenStack sevice-types-authority data" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "os-service-types-1.7.0.tar.gz", hash = "sha256:31800299a82239363995b91f1ebf9106ac7758542a1e4ef6dc737a5932878c6c"}, {file = "os_service_types-1.7.0-py2.py3-none-any.whl", hash = "sha256:0505c72205690910077fb72b88f2a1f07533c8d39f2fe75b29583481764965d6"}, @@ -2002,6 +2071,7 @@ version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["dev", "docs"] files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, @@ -2013,6 +2083,7 @@ version = "0.5.6" description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, ] @@ -2023,6 +2094,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -2034,6 +2106,7 @@ version = "6.0.0" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" +groups = ["main"] files = [ {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, @@ -2045,6 +2118,7 @@ version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, @@ -2060,6 +2134,7 @@ 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"}, @@ -2075,6 +2150,7 @@ version = "0.21.0" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166"}, {file = "prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"}, @@ -2089,6 +2165,7 @@ version = "1.23.0" description = "Beautiful, Pythonic protocol buffers." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "proto-plus-1.23.0.tar.gz", hash = "sha256:89075171ef11988b3fa157f5dbd8b9cf09d65fffee97e29ce403cd8defba19d2"}, {file = "proto_plus-1.23.0-py3-none-any.whl", hash = "sha256:a829c79e619e1cf632de091013a4173deed13a55f326ef84f05af6f50ff4c82c"}, @@ -2106,6 +2183,7 @@ version = "4.25.3" description = "" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, @@ -2126,6 +2204,7 @@ version = "0.5.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] files = [ {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, @@ -2137,6 +2216,7 @@ version = "0.3.0" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] files = [ {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, @@ -2151,6 +2231,8 @@ version = "2.21" description = "C parser in Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main", "dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -2162,6 +2244,7 @@ version = "1.10.18" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"}, {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"}, @@ -2221,13 +2304,14 @@ version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] -plugins = ["importlib-metadata"] +plugins = ["importlib-metadata ; python_version < \"3.8\""] windows-terminal = ["colorama (>=0.4.6)"] [[package]] @@ -2236,6 +2320,7 @@ version = "2.8.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, @@ -2256,6 +2341,7 @@ version = "10.7.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, @@ -2274,6 +2360,7 @@ version = "24.2.1" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, @@ -2292,6 +2379,7 @@ version = "1.1.383" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pyright-1.1.383-py3-none-any.whl", hash = "sha256:d864d1182a313f45aaf99e9bfc7d2668eeabc99b29a556b5344894fd73cb1959"}, {file = "pyright-1.1.383.tar.gz", hash = "sha256:1df7f12407f3710c9c6df938d98ec53f70053e6c6bbf71ce7bcb038d42f10070"}, @@ -2312,6 +2400,7 @@ version = "8.3.3" 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"}, @@ -2332,6 +2421,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"}, @@ -2350,6 +2440,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "docs"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2364,6 +2455,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -2378,6 +2470,7 @@ version = "0.0.9" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, @@ -2392,6 +2485,7 @@ version = "1.1.0" description = "Universally Unique Lexicographically Sortable Identifier" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "python-ulid-1.1.0.tar.gz", hash = "sha256:5fb5e4a91db8ca93e8938a613360b3def299b60d41f847279a8c39c9b2e9c65e"}, {file = "python_ulid-1.1.0-py3-none-any.whl", hash = "sha256:88c952f6be133dbede19c907d72d26717d2691ec8421512b573144794d891e24"}, @@ -2403,6 +2497,7 @@ version = "8.0.3.0.1" description = "VMware vSphere Python SDK" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7.9" +groups = ["main"] files = [ {file = "pyvmomi-8.0.3.0.1.tar.gz", hash = "sha256:db795c960159cfa3c81e6af4cf1f46618e61cf0349db1666de75df98a4f29c69"}, ] @@ -2419,6 +2514,8 @@ version = "306" description = "Python for Window Extensions" optional = false python-versions = "*" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, @@ -2442,6 +2539,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -2504,6 +2602,7 @@ version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, @@ -2518,6 +2617,7 @@ version = "5.1.1" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24"}, {file = "redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72"}, @@ -2536,6 +2636,7 @@ version = "0.3.1" description = "Object mappings, and more, for Redis." optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ {file = "redis_om-0.3.1-py3-none-any.whl", hash = "sha256:c521b4e60d7bbdf537642f5b94d004330a095dcc1e4daf6efec8e46b0a2f2799"}, {file = "redis_om-0.3.1.tar.gz", hash = "sha256:1a1eea45a507da3541a6afa982c7aecae2d58920c756525198917afc433504ee"}, @@ -2558,6 +2659,7 @@ version = "0.33.0" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "referencing-0.33.0-py3-none-any.whl", hash = "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5"}, {file = "referencing-0.33.0.tar.gz", hash = "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7"}, @@ -2573,6 +2675,7 @@ version = "2023.12.25" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, @@ -2675,6 +2778,7 @@ version = "2.32.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, @@ -2696,6 +2800,7 @@ version = "1.4.0" description = "Import exceptions from potentially bundled packages in requests." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "requestsexceptions-1.4.0-py2.py3-none-any.whl", hash = "sha256:3083d872b6e07dc5c323563ef37671d992214ad9a32b0ca4a3d7f5500bf38ce3"}, {file = "requestsexceptions-1.4.0.tar.gz", hash = "sha256:b095cbc77618f066d459a02b137b020c37da9f46d9b057704019c9f77dba3065"}, @@ -2707,6 +2812,7 @@ version = "0.18.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, @@ -2815,6 +2921,7 @@ version = "1.16.2" description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "rq-1.16.2-py3-none-any.whl", hash = "sha256:52e619f6cb469b00e04da74305045d244b75fecb2ecaa4f26422add57d3c5f09"}, {file = "rq-1.16.2.tar.gz", hash = "sha256:5c5b9ad5fbaf792b8fada25cc7627f4d206a9a4455aced371d4f501cc3f13b34"}, @@ -2830,6 +2937,7 @@ version = "0.13.1" description = "Provides job scheduling capabilities to RQ (Redis Queue)" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "rq-scheduler-0.13.1.tar.gz", hash = "sha256:89d6a18f215536362b22c0548db7dbb8678bc520c18dc18a82fd0bb2b91695ce"}, {file = "rq_scheduler-0.13.1-py2.py3-none-any.whl", hash = "sha256:c2b19c3aedfc7de4d405183c98aa327506e423bf4cdc556af55aaab9bbe5d1a1"}, @@ -2847,6 +2955,7 @@ version = "4.9" description = "Pure-Python RSA implementation" optional = false python-versions = ">=3.6,<4" +groups = ["main"] files = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, @@ -2861,6 +2970,7 @@ version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, @@ -2888,6 +2998,7 @@ version = "0.10.0" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">= 3.8" +groups = ["main"] files = [ {file = "s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e"}, {file = "s3transfer-0.10.0.tar.gz", hash = "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"}, @@ -2899,12 +3010,45 @@ botocore = ">=1.33.2,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] +[[package]] +name = "scaleway" +version = "2.10.3" +description = "Scaleway SDK for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "scaleway-2.10.3-py3-none-any.whl", hash = "sha256:dbf381440d6caf37c878cf16445a63f4969a4aac2257c9b72c744d10ff223a0c"}, + {file = "scaleway-2.10.3.tar.gz", hash = "sha256:b1f9dd1b1450767205234c6f5a345e5e25dc039c780253d698893b5c344ce594"}, +] + +[package.dependencies] +scaleway-core = "2.10.3" + +[[package]] +name = "scaleway-core" +version = "2.10.3" +description = "Scaleway SDK for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "scaleway_core-2.10.3-py3-none-any.whl", hash = "sha256:fd4112144554d6adae22ff737555eeb0e38cb1063250b3e88c9aebc1b957793b"}, + {file = "scaleway_core-2.10.3.tar.gz", hash = "sha256:56432f755d694669429de51d51c1d0b3361b28dc2f939b28e4cb954610ee76be"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.2,<3.0.0" +PyYAML = ">=6.0,<7.0" +requests = ">=2.28.1,<3.0.0" + [[package]] name = "setuptools" version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, @@ -2912,7 +3056,7 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -2921,6 +3065,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main", "docs"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -2932,6 +3077,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2943,6 +3089,7 @@ version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, @@ -2954,6 +3101,7 @@ version = "0.37.2" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, @@ -2971,6 +3119,7 @@ version = "5.2.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, @@ -2985,6 +3134,7 @@ version = "0.20.5" description = "Type annotations and code completion for awscrt" optional = false python-versions = ">=3.7,<4.0" +groups = ["main"] files = [ {file = "types_awscrt-0.20.5-py3-none-any.whl", hash = "sha256:79d5bfb01f64701b6cf442e89a37d9c4dc6dbb79a46f2f611739b2418d30ecfd"}, {file = "types_awscrt-0.20.5.tar.gz", hash = "sha256:61811bbf4de95248939f9276a434be93d2b95f6ccfe8aa94e56999e9778cfcc2"}, @@ -2996,6 +3146,7 @@ version = "24.0.0.20240228" description = "Typing stubs for pyOpenSSL" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "types-pyOpenSSL-24.0.0.20240228.tar.gz", hash = "sha256:cd990717d8aa3743ef0e73e0f462e64b54d90c304249232d48fece4f0f7c3c6a"}, {file = "types_pyOpenSSL-24.0.0.20240228-py3-none-any.whl", hash = "sha256:a472cf877a873549175e81972f153f44e975302a3cf17381eb5f3d41ccfb75a4"}, @@ -3010,6 +3161,7 @@ version = "4.6.0.20241004" description = "Typing stubs for redis" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e"}, {file = "types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed"}, @@ -3025,6 +3177,7 @@ version = "0.10.0" description = "Type annotations and code completion for s3transfer" optional = false python-versions = ">=3.7,<4.0" +groups = ["main"] files = [ {file = "types_s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f"}, {file = "types_s3transfer-0.10.0.tar.gz", hash = "sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69"}, @@ -3036,6 +3189,7 @@ version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, @@ -3047,13 +3201,14 @@ version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3064,6 +3219,7 @@ version = "0.31.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"}, {file = "uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906"}, @@ -3076,12 +3232,12 @@ h11 = ">=0.8" httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "uvloop" @@ -3089,6 +3245,8 @@ version = "0.19.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" files = [ {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, @@ -3125,7 +3283,7 @@ files = [ [package.extras] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0) ; python_version >= \"3.12\"", "aiohttp (>=3.8.1) ; python_version < \"3.12\"", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] [[package]] name = "vapi-runtime" @@ -3133,6 +3291,7 @@ version = "2.40.0" description = "vAPI Runtime" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "vapi_runtime-2.40.0-py2.py3-none-any.whl", hash = "sha256:89ac5f61858d6a3a452b8ba28e64da7956aeb25e530ee17d1ad1c6c256df49f6"}, ] @@ -3156,6 +3315,7 @@ version = "4.1.0" description = "vapi client bindings for VMware vSphere Automation" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "vcenter_bindings-4.1.0-py2.py3-none-any.whl", hash = "sha256:3019a76128019e2b31066e03307eff415e8d34e96ac5589c81e73dbac3834cc3"}, ] @@ -3174,6 +3334,7 @@ version = "4.0.0" description = "Filesystem events monitoring" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, @@ -3215,6 +3376,7 @@ version = "0.21.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, @@ -3302,6 +3464,7 @@ version = "12.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, @@ -3383,6 +3546,7 @@ version = "3.0.3" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, @@ -3400,6 +3564,7 @@ version = "1.35.1" description = "A linter for YAML files." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"}, {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"}, @@ -3413,6 +3578,6 @@ pyyaml = "*" dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.11" -content-hash = "3b0e36055af948122c3bbaf6709d730b54dd08b87946508b670213a2696da594" +content-hash = "f7bd8456434fccdb7885aceaa0f8a15ef4f4bd0ab2dbe320d4d76131b0a89598" diff --git a/pyproject.toml b/pyproject.toml index 25f11a51..2d3df2b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ pyvmomi = "^8.0.3.0.1" vapi-runtime = { url = "https://raw.githubusercontent.com/vmware/vsphere-automation-sdk-python/v8.0.1.0/lib/vapi-runtime/vapi_runtime-2.40.0-py2.py3-none-any.whl" } vcenter-bindings = { url = "https://raw.githubusercontent.com/vmware/vsphere-automation-sdk-python/v8.0.1.0/lib/vcenter-bindings/vcenter_bindings-4.1.0-py2.py3-none-any.whl" } prometheus-client = "^0.21.0" +scaleway = "^2.10.0" [tool.poetry.group.docs] diff --git a/runner_manager/backend/scaleway.py b/runner_manager/backend/scaleway.py new file mode 100644 index 00000000..67d98694 --- /dev/null +++ b/runner_manager/backend/scaleway.py @@ -0,0 +1,425 @@ +# pyright: reportOptionalMemberAccess=false, reportArgumentType=false, reportReturnType=false, reportMissingTypeStubs=false +import logging +import os +import re +import time +from typing import List, Literal + +from pydantic import Field +from redis_om import NotFoundError +from scaleway import Client # type: ignore[import-untyped] +from scaleway.instance.v1 import ( # type: ignore[import-untyped] + Image, + Server, + ServerAction, + ServerState, +) +from scaleway.instance.v1.custom_api import ( + InstanceUtilsV1API, # type: ignore[import-untyped] +) + +from runner_manager.backend.base import BaseBackend +from runner_manager.models.backend import ( + Backends, + ScalewayConfig, + ScalewayInstanceConfig, +) +from runner_manager.models.runner import Runner + +log = logging.getLogger(__name__) + + +class ScalewayBackend(BaseBackend): + """Backend for Scaleway cloud provider.""" + + name: Literal[Backends.scaleway] = Field(default=Backends.scaleway) + config: ScalewayConfig + instance_config: ScalewayInstanceConfig + + @property + def client(self) -> InstanceUtilsV1API: + """Returns a Scaleway Instance API client.""" + access_key = self.config.access_key or os.getenv("SCW_ACCESS_KEY") + secret_key = self.config.secret_key or os.getenv("SCW_SECRET_KEY") + + if not access_key or not secret_key: + raise ValueError( + "Scaleway credentials not found. Set SCW_ACCESS_KEY and SCW_SECRET_KEY." + ) + + scw_client = Client( + access_key=access_key, + secret_key=secret_key, + default_project_id=self.config.project_id, + default_zone=self.config.zone, + default_region=self.config.region, + ) + return InstanceUtilsV1API(scw_client) + + def sanitize_tags(self, tags: List[str]) -> List[str]: + """Sanitize tags to comply with Scaleway requirements. + + Scaleway tags must: + - Be lowercase + - Contain only alphanumeric characters, hyphens, and underscores + - Not exceed 255 characters + """ + sanitized = [] + for tag in tags: + # Convert to lowercase and replace invalid characters + sanitized_tag = re.sub(r"[^a-z0-9\-_]", "-", tag.lower()) + # Limit length + sanitized_tag = sanitized_tag[:255] + sanitized.append(sanitized_tag) + return sanitized + + def get_image(self, image_name: str) -> Image: + """Get image by name or ID.""" + try: + # Try to get by ID first + return self.client.get_image( + zone=self.config.zone, + image_id=image_name, + ).image + except Exception: + # Otherwise, list images and find by name + images = self.client.list_images( + zone=self.config.zone, + name=image_name, + ).images + if images: + return images[0] + raise ValueError( + f"Image '{image_name}' not found in zone {self.config.zone}" + ) + + def wait_for_server_state( + self, + server_id: str, + target_state: ServerState, + timeout: int = 300, + ) -> Server: + """Wait for server to reach target state.""" + start_time = time.time() + while time.time() - start_time < timeout: + server = self.client.get_server( + zone=self.config.zone, + server_id=server_id, + ).server + + if server.state == target_state: + return server + + time.sleep(2) + + raise TimeoutError( + f"Server {server_id} did not reach state {target_state} within {timeout} seconds" + ) + + def create(self, runner: Runner) -> Runner: + """Create a runner instance on Scaleway. + + Args: + runner (Runner): Runner instance to be created. + + Returns: + Runner: Updated runner with instance_id. + """ + log.info(f"Creating Scaleway instance for runner {runner.name}") + + # Get image + image = self.get_image(self.instance_config.image) + + # Prepare tags + tags = [ + f"manager={self.manager or runner.manager}", + f"runner-group={self.runner_group or runner.runner_group_name}", + f"name={runner.name}", + f"status={runner.status}", + f"busy={str(runner.busy)}", + ] + self.instance_config.tags + + tags = self.sanitize_tags(tags) + + # Add SSH public key as AUTHORIZED_KEY tag for debugging access + if self.instance_config.ssh_public_key: + parts = self.instance_config.ssh_public_key.split() + if len(parts) >= 2: + ssh_key_for_tag = f"{parts[0]}_{parts[1]}" + tags.append(f"AUTHORIZED_KEY={ssh_key_for_tag}") + log.info( + "Adding SSH public key via AUTHORIZED_KEY tag for debugging access" + ) + else: + log.warning( + "Invalid SSH public key format, skipping AUTHORIZED_KEY tag" + ) + + # Prepare security group ID if provided + security_group = None + if self.instance_config.security_group_ids: + security_group = self.instance_config.security_group_ids[0] + + # Determine IP strategy: Public Gateway vs Direct Public IP + # If public_gateway_id is provided, use private network with gateway + # Otherwise, use direct public IP if enabled + use_gateway = bool(self.instance_config.public_gateway_id) + dynamic_ip_required = self.instance_config.enable_public_ip and not use_gateway + + # Create server using _create_server + # Note: In SDK 2.10.3, 'protected' is a required parameter + response = self.client._create_server( + commercial_type=self.instance_config.commercial_type, + image=image.id, + enable_ipv6=self.instance_config.enable_ipv6, + name=runner.name, + protected=False, + dynamic_ip_required=dynamic_ip_required, + tags=tags, + boot_type=self.instance_config.boot_type, + project=self.config.project_id, + organization=self.config.organization_id, + security_group=security_group, + ) + + server = response.server + log.info(f"Server created with ID: {server.id}") + + # If using public gateway, attach to private network + if use_gateway and self.instance_config.private_network_id: + try: + # Attach server to private network using Instance API + self.client.create_private_nic( + server_id=server.id, + private_network_id=self.instance_config.private_network_id, + ) + log.info( + f"Server {server.id} attached to private network {self.instance_config.private_network_id}" + ) + except Exception as e: + log.warning( + f"Failed to attach server to private network: {e}. Server will use public IP." + ) + + # Set cloud-init user data using SDK's InstanceUtilsV1API.set_server_user_data + # This is similar to GCP's startup-script metadata + if runner.encoded_jit_config: + startup_script = self.instance_config.template_startup(runner) + try: + self.client.set_server_user_data( + server_id=server.id, + key="cloud-init", + content=startup_script.encode("utf-8"), + zone=self.config.zone, + ) + log.info(f"Cloud-init user data set for server {server.id}") + except Exception as e: + log.warning(f"Failed to set cloud-init user data: {e}") + # Continue anyway, the instance is created + else: + log.warning( + f"No JIT config provided for runner {runner.name}, skipping startup script" + ) + + # Start the server + self.client.server_action( + zone=self.config.zone, + server_id=server.id, + action=ServerAction.POWERON, + ) + + log.info(f"Starting server {server.id}") + + # Wait for server to be running + try: + self.wait_for_server_state(server.id, ServerState.RUNNING) + log.info(f"Server {server.id} is now running") + except TimeoutError as e: + log.error(f"Error waiting for server to start: {e}") + + # Save instance ID + runner.instance_id = server.id + + return super().create(runner) + + def delete(self, runner: Runner) -> int: + """Delete a runner instance from Scaleway. + + Args: + runner (Runner): Runner instance to be deleted. + + Returns: + int: Number of deleted runners. + """ + if not runner.instance_id: + log.warning(f"Runner {runner.name} has no instance_id, skipping deletion") + return super().delete(runner) + + log.info(f"Deleting Scaleway instance {runner.instance_id}") + + try: + # Get server to check if it exists + server = self.client.get_server( + zone=self.config.zone, + server_id=runner.instance_id, + ).server + + # Collect volume IDs before deletion + volume_ids = [] + if server.volumes: + # server.volumes is a dict with keys like "0", "1", etc. + # and values are Volume objects with .id attribute + volume_ids = [vol.id for vol in server.volumes.values()] + log.info(f"Server has {len(volume_ids)} volume(s) that will be deleted") + + # Power off the server if it's running + if server.state in [ServerState.RUNNING, ServerState.STARTING]: + log.info(f"Powering off server {runner.instance_id}") + self.client.server_action( + zone=self.config.zone, + server_id=runner.instance_id, + action=ServerAction.POWEROFF, + ) + # Wait for server to be stopped + self.wait_for_server_state( + runner.instance_id, ServerState.STOPPED, timeout=120 + ) + + # Delete the server + self.client.delete_server( + zone=self.config.zone, + server_id=runner.instance_id, + ) + log.info(f"Server {runner.instance_id} deleted successfully") + + # Delete associated volumes + for volume_id in volume_ids: + try: + self.client.delete_volume( + zone=self.config.zone, + volume_id=volume_id, + ) + log.info(f"Volume {volume_id} deleted successfully") + except Exception as vol_error: + log.warning(f"Failed to delete volume {volume_id}: {vol_error}") + + except Exception as e: + if "404" in str(e) or "not found" in str(e).lower(): + log.warning( + f"Server {runner.instance_id} not found, may have been already deleted" + ) + else: + log.error(f"Error deleting server {runner.instance_id}: {e}") + raise + + return super().delete(runner) + + def update(self, runner: Runner, webhook) -> Runner: + """Update a runner instance on Scaleway. + + Args: + runner (Runner): Runner instance to be updated. + webhook: Webhook event data. + + Returns: + Runner: Updated runner. + """ + if not runner.instance_id: + log.warning(f"Runner {runner.name} has no instance_id, skipping update") + return super().update(runner, webhook) + + try: + # Update tags + tags = self.sanitize_tags( + [ + f"manager={self.manager or runner.manager}", + f"runner-group={self.runner_group or runner.runner_group_name}", + f"name={runner.name}", + f"status={runner.status}", + f"busy={str(runner.busy)}", + ] + + self.instance_config.tags + ) + + self.client._update_server( + zone=self.config.zone, + server_id=runner.instance_id, + tags=tags, + ) + log.info(f"Updated tags for server {runner.instance_id}") + + except Exception as e: + log.error(f"Error updating server {runner.instance_id}: {e}") + raise + + return super().update(runner, webhook) + + def get(self, instance_id: str) -> Runner: + """Get a runner instance by instance ID. + + Args: + instance_id (str): Instance ID to retrieve. + + Returns: + Runner: Runner instance. + + Raises: + NotFoundError: If instance is not found. + """ + runner = Runner.find(Runner.instance_id == instance_id).first() + if not runner: + raise NotFoundError( + f"Runner with instance_id {instance_id} not found in database" + ) + return runner + + def list(self) -> List[Runner]: + """List all runner instances from Scaleway. + + Returns: + List[Runner]: List of runner instances. + """ + runners = [] + + try: + # List all servers in the project/zone + servers = self.client.list_servers( + zone=self.config.zone, + project=self.config.project_id, + ).servers + + # Filter servers by tags (sanitized format) + for server in servers: + # Check if this server is managed by this runner manager + # Tags are sanitized, so "=" becomes "-" + manager_tag = self.sanitize_tags([f"manager={self.manager}"])[0] + group_tag = self.sanitize_tags([f"runner-group={self.runner_group}"])[0] + + if manager_tag in server.tags and group_tag in server.tags: + # Find corresponding runner in database + try: + runner = Runner.find(Runner.instance_id == server.id).first() + except NotFoundError: + # Create runner if not found (sync from cloud) + # Extract name from tags + name = server.name + for tag in server.tags: + if tag.startswith("name-"): + name = tag[5:] # Remove "name-" prefix + break + + runner = Runner( + name=name, + instance_id=server.id, + runner_group_name=self.runner_group, + busy=any("busy-true" in tag for tag in server.tags), + status="online", + created_at=server.creation_date, + started_at=server.creation_date, + ) + runners.append(runner) + + except Exception as e: + log.error(f"Error listing Scaleway servers: {e}") + + return runners diff --git a/runner_manager/models/backend.py b/runner_manager/models/backend.py index 98bb60f1..0a219027 100644 --- a/runner_manager/models/backend.py +++ b/runner_manager/models/backend.py @@ -31,6 +31,7 @@ class Backends(str, Enum): aws = "aws" openstack = "openstack" vsphere = "vsphere" + scaleway = "scaleway" class BackendConfig(BaseModel): @@ -300,3 +301,32 @@ class VsphereInstanceConfig(InstanceConfig): datastore: str library: str library_item: str + + +class ScalewayConfig(BackendConfig): + """Configuration for Scaleway backend.""" + + access_key: Optional[str] = None + secret_key: Optional[str] = None + project_id: str + zone: str = "fr-par-1" + region: str = "fr-par" + organization_id: Optional[str] = None + + +class ScalewayInstanceConfig(InstanceConfig): + """Configuration for Scaleway backend instance.""" + + commercial_type: str = "DEV1-S" + image: str = "ubuntu_jammy" + enable_ipv6: bool = False + enable_public_ip: bool = True + public_gateway_id: Optional[str] = None + private_network_id: Optional[str] = None + security_group_ids: List[str] = [] + ssh_public_key: Optional[str] = ( + None # SSH public key for debugging (added as AUTHORIZED_KEY tag) + ) + boot_type: str = "local" + volumes: Dict[str, str] = {} + tags: List[str] = [] diff --git a/runner_manager/models/runner_group.py b/runner_manager/models/runner_group.py index bda351b3..4e76d90d 100644 --- a/runner_manager/models/runner_group.py +++ b/runner_manager/models/runner_group.py @@ -19,6 +19,7 @@ from runner_manager.backend.docker import DockerBackend from runner_manager.backend.gcloud import GCPBackend from runner_manager.backend.openstack import OpenstackBackend +from runner_manager.backend.scaleway import ScalewayBackend from runner_manager.backend.vsphere import VsphereBackend from runner_manager.clients.github import GitHub from runner_manager.clients.github import RunnerGroup as GitHubRunnerGroup @@ -58,6 +59,7 @@ class BaseRunnerGroup(PydanticBaseModel): AWSBackend, VsphereBackend, OpenstackBackend, + ScalewayBackend, ], PydanticField(..., discriminator="name"), ] diff --git a/tests/unit/backend/test_scaleway.py b/tests/unit/backend/test_scaleway.py new file mode 100644 index 00000000..81ecc563 --- /dev/null +++ b/tests/unit/backend/test_scaleway.py @@ -0,0 +1,519 @@ +import os +from unittest.mock import MagicMock + +import pytest +from redis_om import NotFoundError +from scaleway.instance.v1 import ServerState + +from runner_manager.backend.scaleway import ScalewayBackend +from runner_manager.models.backend import ( + Backends, + ScalewayConfig, + ScalewayInstanceConfig, +) +from runner_manager.models.runner import Runner +from runner_manager.models.runner_group import RunnerGroup + + +@pytest.fixture() +def scaleway_group(settings) -> RunnerGroup: + """Create a runner group with Scaleway backend configuration.""" + config = ScalewayConfig( + project_id=os.environ.get("SCW_DEFAULT_PROJECT_ID", "test-project-id"), + zone=os.environ.get("SCW_DEFAULT_ZONE", "fr-par-1"), + region=os.environ.get("SCW_DEFAULT_REGION", "fr-par"), + ) + runner_group = RunnerGroup( + id=2, + name="test-scaleway", + organization="octo-org", + manager=settings.name, + backend=ScalewayBackend( + name=Backends.scaleway, + config=config, + instance_config=ScalewayInstanceConfig( + commercial_type="DEV1-S", + # Use the actual UUID for Ubuntu 22.04 Jammy in fr-par-1 + image=os.environ.get( + "SCW_IMAGE_ID", "ec31d73d-ca36-4536-adf4-0feb76d30379" + ), + tags=["test", "runner-manager"], + ), + ), + labels=["scaleway", "test"], + ) + return runner_group + + +@pytest.fixture() +def fake_scaleway_group(scaleway_group, monkeypatch): + """Mock the Scaleway client for tests without API.""" + + # Mock Image + mock_image = MagicMock() + mock_image.id = "test-image-id" + mock_image.name = "ubuntu_jammy" + + # Mock Server + mock_server = MagicMock() + mock_server.id = "test-server-id" + mock_server.name = "test-runner" + mock_server.state = ServerState.RUNNING + mock_server.tags = ["manager=test", "runner-group=test-scaleway"] + + # Mock API responses + mock_client = MagicMock() + mock_client.get_image.return_value = MagicMock(image=mock_image) + mock_client.list_images.return_value = MagicMock(images=[mock_image]) + mock_client._create_server.return_value = MagicMock(server=mock_server) + mock_client.get_server.return_value = MagicMock(server=mock_server) + mock_client.list_servers.return_value = MagicMock(servers=[mock_server]) + mock_client.set_server_user_data.return_value = None + mock_client.server_action.return_value = None + mock_client.update_server.return_value = None + mock_client.delete_server.return_value = None + + # Patch the client property + monkeypatch.setattr(ScalewayBackend, "client", property(lambda self: mock_client)) + + # Mock wait_for_server_state to return immediately + def mock_wait(self, server_id, target_state, timeout=300): + return mock_server + + monkeypatch.setattr(ScalewayBackend, "wait_for_server_state", mock_wait) + + return scaleway_group + + +@pytest.fixture() +def scaleway_runner(runner: Runner, scaleway_group: RunnerGroup) -> Runner: + """Use the standard test runner.""" + # Cleanup and return a runner for testing + scaleway_group.backend.delete(runner) + return runner + + +def test_sanitize_tags(fake_scaleway_group): + """Test tag sanitization for Scaleway compliance.""" + backend = fake_scaleway_group.backend + + # Test with invalid characters + tags = ["Test-Tag", "with spaces", "UPPERCASE", "special@chars!", "underscore_ok"] + sanitized = backend.sanitize_tags(tags) + + assert "test-tag" in sanitized + assert "with-spaces" in sanitized + assert "uppercase" in sanitized + assert "special-chars-" in sanitized + assert "underscore_ok" in sanitized + + # All tags should be lowercase + for tag in sanitized: + assert tag == tag.lower() + + # Test length limitation + long_tag = "a" * 300 + sanitized_long = backend.sanitize_tags([long_tag]) + assert len(sanitized_long[0]) == 255 + + +def test_backend_name(fake_scaleway_group): + """Test backend name is correctly set.""" + assert fake_scaleway_group.backend.name == Backends.scaleway + assert fake_scaleway_group.backend.name == "scaleway" + + +def test_get_image(fake_scaleway_group): + """Test getting image by name.""" + backend = fake_scaleway_group.backend + image = backend.get_image("ubuntu_jammy") + + assert image.id == "test-image-id" + assert image.name == "ubuntu_jammy" + + +def test_create_instance_mock(scaleway_runner, fake_scaleway_group): + """Test instance creation with mocked client.""" + backend = fake_scaleway_group.backend + runner = backend.create(scaleway_runner) + + assert runner.instance_id == "test-server-id" + assert runner.backend == "scaleway" + + +def test_delete_instance_mock(scaleway_runner, fake_scaleway_group): + """Test instance deletion with mocked client.""" + backend = fake_scaleway_group.backend + scaleway_runner.instance_id = "test-server-id" + scaleway_runner.save() + + result = backend.delete(scaleway_runner) + assert result == 1 + + +def test_update_instance_mock(scaleway_runner, fake_scaleway_group): + """Test instance update with mocked client.""" + backend = fake_scaleway_group.backend + scaleway_runner.instance_id = "test-server-id" + scaleway_runner.save() + + runner = backend.update(scaleway_runner, None) + assert runner.instance_id == "test-server-id" + + +def test_list_instances_mock(scaleway_runner, fake_scaleway_group): + """Test listing instances with mocked client.""" + backend = fake_scaleway_group.backend + scaleway_runner.instance_id = "test-server-id" + scaleway_runner.save() + + runners = backend.list() + assert isinstance(runners, list) + + +def test_wait_for_server_state_timeout(fake_scaleway_group, monkeypatch): + """Test wait_for_server_state timeout.""" + + # Mock get_server to return a server in STARTING state + mock_server = MagicMock() + mock_server.state = ServerState.STARTING + mock_client = MagicMock() + mock_client.get_server.return_value = MagicMock(server=mock_server) + monkeypatch.setattr(ScalewayBackend, "client", property(lambda self: mock_client)) + + # Restore the real wait_for_server_state method + monkeypatch.undo() + monkeypatch.setattr(ScalewayBackend, "client", property(lambda self: mock_client)) + + backend_instance = ScalewayBackend( + name=Backends.scaleway, + config=fake_scaleway_group.backend.config, + instance_config=fake_scaleway_group.backend.instance_config, + ) + + with pytest.raises(TimeoutError): + backend_instance.wait_for_server_state( + "test-server-id", ServerState.RUNNING, timeout=1 + ) + + +def test_create_with_ssh_key(scaleway_runner, fake_scaleway_group, monkeypatch): + """Test instance creation with SSH public key.""" + # Add SSH public key to config + fake_scaleway_group.backend.instance_config.ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAbCdEfGhIjKlMnOpQrStUvWxYz user@example.com" + + backend = fake_scaleway_group.backend + runner = backend.create(scaleway_runner) + + assert runner.instance_id == "test-server-id" + # Verify AUTHORIZED_KEY tag was added + mock_client = backend.client + create_call = mock_client._create_server.call_args + tags = create_call.kwargs.get("tags", []) + assert any(tag.startswith("AUTHORIZED_KEY=") for tag in tags) + + +def test_create_with_invalid_ssh_key(scaleway_runner, fake_scaleway_group, caplog): + """Test instance creation with invalid SSH public key format.""" + # Add invalid SSH public key to config + fake_scaleway_group.backend.instance_config.ssh_public_key = "invalid-key" + + backend = fake_scaleway_group.backend + runner = backend.create(scaleway_runner) + + assert runner.instance_id == "test-server-id" + # Verify warning was logged + assert "Invalid SSH public key format" in caplog.text + + +def test_create_with_public_gateway(scaleway_runner, fake_scaleway_group): + """Test instance creation with public gateway.""" + # Add public gateway configuration + fake_scaleway_group.backend.instance_config.public_gateway_id = "test-gateway-id" + fake_scaleway_group.backend.instance_config.private_network_id = "test-network-id" + + backend = fake_scaleway_group.backend + mock_client = backend.client + mock_client.create_private_nic.return_value = None + + runner = backend.create(scaleway_runner) + + assert runner.instance_id == "test-server-id" + # Verify private NIC was created + mock_client.create_private_nic.assert_called_once() + + +def test_create_with_public_gateway_failure( + scaleway_runner, fake_scaleway_group, caplog, monkeypatch +): + """Test instance creation with public gateway attachment failure.""" + # Add public gateway configuration + fake_scaleway_group.backend.instance_config.public_gateway_id = "test-gateway-id" + fake_scaleway_group.backend.instance_config.private_network_id = "test-network-id" + + backend = fake_scaleway_group.backend + mock_client = backend.client + mock_client.create_private_nic.side_effect = Exception("Network error") + + runner = backend.create(scaleway_runner) + + assert runner.instance_id == "test-server-id" + # Verify warning was logged + assert "Failed to attach server to private network" in caplog.text + + +def test_create_without_jit_config(scaleway_runner, fake_scaleway_group, caplog): + """Test instance creation without JIT config.""" + # Remove JIT config + scaleway_runner.encoded_jit_config = None + + backend = fake_scaleway_group.backend + created_runner = backend.create(scaleway_runner) + + assert created_runner.instance_id == "test-server-id" + # Verify warning was logged + assert "No JIT config provided" in caplog.text + + +def test_delete_with_volumes(scaleway_runner, fake_scaleway_group, monkeypatch): + """Test instance deletion with attached volumes.""" + backend = fake_scaleway_group.backend + scaleway_runner.instance_id = "test-server-id" + scaleway_runner.save() + + # Mock server with volumes + mock_volume = MagicMock() + mock_volume.id = "test-volume-id" + mock_server = MagicMock() + mock_server.id = "test-server-id" + mock_server.state = ServerState.RUNNING + mock_server.volumes = {"0": mock_volume} + + mock_client = backend.client + mock_client.get_server.return_value = MagicMock(server=mock_server) + mock_client.delete_volume.return_value = None + + # Restore wait_for_server_state mock + def mock_wait(self, server_id, target_state, timeout=300): + return mock_server + + monkeypatch.setattr(ScalewayBackend, "wait_for_server_state", mock_wait) + + result = backend.delete(scaleway_runner) + + # Verify volume was deleted + mock_client.delete_volume.assert_called_once_with( + zone=backend.config.zone, + volume_id="test-volume-id", + ) + assert result == 1 + + +def test_delete_with_volume_error( + scaleway_runner, fake_scaleway_group, caplog, monkeypatch +): + """Test instance deletion with volume deletion error.""" + backend = fake_scaleway_group.backend + scaleway_runner.instance_id = "test-server-id" + scaleway_runner.save() + + # Mock server with volumes + mock_volume = MagicMock() + mock_volume.id = "test-volume-id" + mock_server = MagicMock() + mock_server.id = "test-server-id" + mock_server.state = ServerState.RUNNING + mock_server.volumes = {"0": mock_volume} + + mock_client = backend.client + mock_client.get_server.return_value = MagicMock(server=mock_server) + mock_client.delete_volume.side_effect = Exception("Volume deletion failed") + + # Restore wait_for_server_state mock + def mock_wait(self, server_id, target_state, timeout=300): + return mock_server + + monkeypatch.setattr(ScalewayBackend, "wait_for_server_state", mock_wait) + + result = backend.delete(scaleway_runner) + + # Verify warning was logged + assert "Failed to delete volume" in caplog.text + assert result == 1 + + +def test_delete_stopped_server(scaleway_runner, fake_scaleway_group): + """Test deletion of already stopped server.""" + backend = fake_scaleway_group.backend + scaleway_runner.instance_id = "test-server-id" + scaleway_runner.save() + + # Mock server in STOPPED state + mock_server = MagicMock() + mock_server.id = "test-server-id" + mock_server.state = ServerState.STOPPED + mock_server.volumes = {} + + mock_client = backend.client + mock_client.get_server.return_value = MagicMock(server=mock_server) + + result = backend.delete(scaleway_runner) + + # Verify server_action POWEROFF was NOT called + assert mock_client.server_action.call_count == 0 + assert result == 1 + + +def test_delete_not_found(scaleway_runner, fake_scaleway_group, caplog): + """Test deletion of non-existent server.""" + backend = fake_scaleway_group.backend + scaleway_runner.instance_id = "non-existent-id" + scaleway_runner.save() + + mock_client = backend.client + mock_client.get_server.side_effect = Exception("404 not found") + + result = backend.delete(scaleway_runner) + + # Verify warning was logged + assert "not found" in caplog.text + assert result == 1 + + +def test_list_with_auto_create(fake_scaleway_group, monkeypatch): + """Test list() creates runners for servers not in database.""" + backend = fake_scaleway_group.backend + + # Mock server not in database + mock_server = MagicMock() + mock_server.id = "new-server-id" + mock_server.name = "new-runner" + mock_server.tags = [ + backend.sanitize_tags([f"manager={backend.manager}"])[0], + backend.sanitize_tags([f"runner-group={backend.runner_group}"])[0], + "name-new-runner", + "busy-true", + ] + mock_server.creation_date = "2026-01-13T10:00:00Z" + + mock_client = backend.client + mock_client.list_servers.return_value = MagicMock(servers=[mock_server]) + + runners = backend.list() + + # Verify a runner was created + assert len(runners) == 1 + assert runners[0].instance_id == "new-server-id" + assert runners[0].name == "new-runner" + assert runners[0].busy is True + + +def test_update_without_instance_id(scaleway_runner, fake_scaleway_group, caplog): + """Test update when runner has no instance_id.""" + backend = fake_scaleway_group.backend + # Don't set instance_id + scaleway_runner.instance_id = None + scaleway_runner.save() + + runner = backend.update(scaleway_runner, None) + + # Verify warning was logged and no update was attempted + assert "no instance_id" in caplog.text + assert runner.instance_id is None + + +def test_get_not_found(fake_scaleway_group): + """Test get() with non-existent instance_id.""" + backend = fake_scaleway_group.backend + + with pytest.raises(NotFoundError): + backend.get("non-existent-id") + + +def test_get_image_by_id(fake_scaleway_group): + """Test get_image with valid image ID.""" + backend = fake_scaleway_group.backend + image = backend.get_image("test-image-id") + + assert image.id == "test-image-id" + + +def test_get_image_not_found(fake_scaleway_group, monkeypatch): + """Test get_image when image is not found.""" + backend = fake_scaleway_group.backend + + mock_client = backend.client + mock_client.get_image.side_effect = Exception("Image not found") + mock_client.list_images.return_value = MagicMock(images=[]) + + with pytest.raises(ValueError, match="not found in zone"): + backend.get_image("non-existent-image") + + +# Real API tests (skipped if credentials not available) + + +@pytest.mark.skipif( + not os.getenv("SCW_ACCESS_KEY"), reason="Scaleway credentials not found" +) +def test_create_delete(scaleway_runner, scaleway_group): + """Test instance creation and deletion. + + This test requires valid Scaleway credentials: + - SCW_ACCESS_KEY + - SCW_SECRET_KEY + - SCW_DEFAULT_PROJECT_ID + """ + # Create instance + runner = scaleway_group.backend.create(scaleway_runner) + + assert runner.instance_id is not None + assert runner.backend == "scaleway" + assert Runner.find(Runner.instance_id == runner.instance_id).first() == runner + + # Delete instance + scaleway_group.backend.delete(runner) + + # Verify deletion from database + with pytest.raises(NotFoundError): + Runner.find(Runner.instance_id == runner.instance_id).first() + + +@pytest.mark.skipif( + not os.getenv("SCW_ACCESS_KEY"), reason="Scaleway credentials not found" +) +def test_update(scaleway_runner, scaleway_group): + """Test instance update.""" + runner = scaleway_group.backend.create(scaleway_runner) + scaleway_group.backend.update(runner, None) + runner = Runner.find(Runner.instance_id == runner.instance_id).first() + scaleway_group.backend.delete(runner) + with pytest.raises(NotFoundError): + scaleway_group.backend.get(runner.instance_id) + + +@pytest.mark.skipif( + not os.getenv("SCW_ACCESS_KEY"), reason="Scaleway credentials not found" +) +def test_get(scaleway_runner, scaleway_group): + """Test instance retrieval.""" + runner = scaleway_group.backend.create(scaleway_runner) + retrieved_runner = scaleway_group.backend.get(runner.instance_id) + assert retrieved_runner.instance_id == runner.instance_id + assert retrieved_runner.name == runner.name + scaleway_group.backend.delete(runner) + with pytest.raises(NotFoundError): + scaleway_group.backend.get(runner.instance_id) + + +@pytest.mark.skipif( + not os.getenv("SCW_ACCESS_KEY"), reason="Scaleway credentials not found" +) +def test_list(scaleway_runner, scaleway_group): + """Test instance listing.""" + runner = scaleway_group.backend.create(scaleway_runner) + runners = scaleway_group.backend.list() + assert runner in runners + scaleway_group.backend.delete(runner) + with pytest.raises(NotFoundError): + scaleway_group.backend.get(runner.instance_id) diff --git a/typings/scaleway/__init__.pyi b/typings/scaleway/__init__.pyi new file mode 100644 index 00000000..9d0f8d86 --- /dev/null +++ b/typings/scaleway/__init__.pyi @@ -0,0 +1,16 @@ +from typing import Optional + +class Client: + def __init__( + self, + access_key: Optional[str] = None, + secret_key: Optional[str] = None, + api_url: str = "https://api.scaleway.com", + api_allow_insecure: bool = False, + user_agent: str = ..., + default_organization_id: Optional[str] = None, + default_project_id: Optional[str] = None, + default_region: Optional[str] = None, + default_zone: Optional[str] = None, + default_page_size: Optional[int] = None, + ) -> None: ... diff --git a/typings/scaleway/instance/__init__.pyi b/typings/scaleway/instance/__init__.pyi new file mode 100644 index 00000000..13b676e7 --- /dev/null +++ b/typings/scaleway/instance/__init__.pyi @@ -0,0 +1 @@ +# Scaleway instance module stub diff --git a/typings/scaleway/instance/v1/__init__.pyi b/typings/scaleway/instance/v1/__init__.pyi new file mode 100644 index 00000000..6c8dee3e --- /dev/null +++ b/typings/scaleway/instance/v1/__init__.pyi @@ -0,0 +1,67 @@ +from enum import Enum +from typing import Optional, List +from dataclasses import dataclass + +class ServerState(str, Enum): + RUNNING: str + STOPPED: str + STARTING: str + STOPPING: str + +class ServerAction(str, Enum): + POWERON: str + POWEROFF: str + REBOOT: str + TERMINATE: str + +@dataclass +class Image: + id: str + name: str + +@dataclass +class Server: + id: str + name: str + state: ServerState + tags: List[str] + +class InstanceV1API: + def __init__(self, client, bypass_validation: bool = False) -> None: ... + def _create_server( + self, + zone: Optional[str] = None, + commercial_type: str = ..., + name: Optional[str] = None, + dynamic_ip_required: Optional[bool] = None, + routed_ip_enabled: Optional[bool] = None, + image: Optional[str] = None, + volumes: Optional[dict] = None, + enable_ipv6: Optional[bool] = None, + protected: bool = False, + public_ip: Optional[str] = None, + public_ips: Optional[List[str]] = None, + boot_type: Optional[str] = None, + organization: Optional[str] = None, + project: Optional[str] = None, + tags: Optional[List[str]] = None, + security_group: Optional[str] = None, + placement_group: Optional[str] = None, + admin_password_encryption_ssh_key_id: Optional[str] = None, + ): ... + def get_server(self, zone: str, server_id: str): ... + def delete_server(self, zone: str, server_id: str) -> None: ... + def server_action( + self, zone: str, server_id: str, action: ServerAction + ) -> None: ... + def get_image(self, zone: str, image_id: str): ... + def list_images(self, zone: str, name: Optional[str] = None): ... + def list_servers( + self, zone: str, project: Optional[str] = None, tags: Optional[List[str]] = None + ): ... + def update_server( + self, zone: str, server_id: str, tags: Optional[List[str]] = None + ): ... + def set_server_user_data( + self, zone: str, server_id: str, key: str, content: bytes + ) -> None: ...