From 972744426cba8ae7663f42bd5f2069d327e722c4 Mon Sep 17 00:00:00 2001 From: ChrisTimperley Date: Mon, 20 May 2024 17:27:08 -0400 Subject: [PATCH] Updated to use poetry, ruff, mypy (1.1.0) and removed Travis config --- .travis.yml | 34 -- CHANGELOG.rst | 4 +- Makefile | 24 +- Pipfile | 24 - Pipfile.lock | 1048 --------------------------------- poetry.lock | 501 ++++++++++++++++ pyproject.toml | 103 ++++ setup.cfg | 78 --- setup.py | 11 - src/dockerblade/__init__.py | 5 +- src/dockerblade/container.py | 98 +-- src/dockerblade/daemon.py | 42 +- src/dockerblade/exceptions.py | 28 +- src/dockerblade/files.py | 306 ++++++---- src/dockerblade/popen.py | 79 +-- src/dockerblade/shell.py | 146 ++--- src/dockerblade/stopwatch.py | 23 +- src/dockerblade/util.py | 7 +- src/dockerblade/version.py | 3 +- 19 files changed, 1024 insertions(+), 1540 deletions(-) delete mode 100644 .travis.yml delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 36f3789..0000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: python -os: linux -dist: xenial -services: -- docker -python: -- '3.6' -install: -- pip install pipenv -- pipenv install --dev -script: -- tox -after_success: -- coveralls -notifications: - email: false -before_deploy: -- python setup.py docs -- touch docs/_build/html/.nojekyll -deploy: -- provider: pypi - username: ChrisTimperley - password: - secure: "h6KGQ4vVEuBSd1mFcrz+WKFbl+nFA9OWUGkJhQVlnb7/nCLees7jBhBIf4AJVcsgB+WZc+qlBGzkso97T9jsO8F5DoHnMnsp3DexaWBZEdnpnTy/EYNsnPix/FpiXlbiLFugVp68kltZ+NZ3bziT8DMaDyRBn29KqISK5ZhWLJv8wnW56sWoO4RNlZBCMOsXlyfGg/De3w2Bw1pIX/P+97b//PsXLmOEjBr2oiOD+j8GkgkT8lzI/MCC8ZUvBJQecKyj0AKSEPqxVkaQtV+x2YYxGIUjASK1eJtaiXYj20phrzmacaz28+FVco8mfnqnO/Ot/cZ+ZqgZpy5Zt1gWlnJ1IuqKKOvFanJcfSc0mSejrv6Uq9MZFIE8RgH5B78ZrGJVYZ/CK5iY6ZERz5Rz8K0Gi8YmXbZAqA1cDJ8tr3JacP/Xyj1gbhwY3oz7+cUrY5XO4UV8MxBzveQvxBhbn5lwJt+9UpB2os+D+0rN8K+BNry+miiXegLzHnui60WNsGhpB9Xe1p1aB9Rrn0qS608ljs5wV+ji2iMDdVTYo0KR2raK7Glmimr6FTZMUiXh7/mqzQzz2JG0W8PM10hHvKyApjrhZgV6+APf/WfgBX4KFw1baqQ0V70dTR84y4Z8OesPYlMKMZ7HQOW/npejGOYQ4h+paF4E/te6I1lR1Us=" - on: - tags: true - python: '3.6' -- provider: pages - skip_cleanup: true - local_dir: docs/_build/html - token: "$GH_TOKEN" - on: - branch: master - python: '3.6' diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eb99236..274585b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,8 @@ -v0.5.8 (XXXX-XX-XX) +v0.6.0 (2024-05-20) ------------------- +* switched from Pipenv to Poetry +* updated linting config to use Ruff and MyPy 1.1.0 * fixed a bug that prevented `time_limit` and `kill_after` from being used by subprocess.check_output diff --git a/Makefile b/Makefile index 5a59378..ac58f22 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,13 @@ -.PHONY: docs check init test tox +.PHONY: check install test lint -# installs pipenv and builds the project -init: - sudo pip install pipenv --upgrade - pipenv install -r requirements.dev.txt - pipenv install --dev +lint: + poetry run ruff check src + poetry run mypy src -# builds the documentation -docs: - pipenv run python setup.py docs - -# runs the unit test suite test: - pipenv run pytest + poetry run pytest -tox: - pipenv run tox +install: + poetry install -# runs a series of checks on the project via tox -check: tox +check: lint test diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 1aaebd8..0000000 --- a/Pipfile +++ /dev/null @@ -1,24 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -dockerblade = {path = ".", editable = true} -mypy = "*" -tox-pipenv = "*" - -[requires] -python_version = "3.9" - -[dev-packages] -mypy = "==0.770" -pytest = "==5.4.2" -ptpython = "==2.0.6" -tox = "==3.14.0" -coveralls = "==1.9.2" -pytest-cov = "==2.8.1" -sphinx-autodoc-typehints = "==1.10.3" -flake8 = "==3.8.2" -Sphinx = "==2.4.4" -twine = "*" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 74ca559..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,1048 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "92f95dd9ed4a1306ff038a645cb0cacbf36543d7f3511f566a7d4c9d03c004cf" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "attrs": { - "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "charset-normalizer": { - "hashes": [ - "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", - "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" - ], - "markers": "python_version >= '3'", - "version": "==2.0.10" - }, - "distlib": { - "hashes": [ - "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b", - "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579" - ], - "version": "==0.3.4" - }, - "docker": { - "hashes": [ - "sha256:d3393c878f575d3a9ca3b94471a3c89a6d960b35feb92f033c0de36cc9d934db", - "sha256:f3607d5695be025fa405a12aca2e5df702a57db63790c73b927eb6a94aac60af" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.4.4" - }, - "dockerblade": { - "editable": true, - "path": "." - }, - "filelock": { - "hashes": [ - "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80", - "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.2" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3'", - "version": "==3.3" - }, - "loguru": { - "hashes": [ - "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319", - "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c" - ], - "markers": "python_version >= '3.5'", - "version": "==0.5.3" - }, - "mslex": { - "hashes": [ - "sha256:380cb14abf8fabf40e56df5c8b21a6d533dc5cbdcfe42406bbf08dda8f42e42a", - "sha256:4a1ac3f25025cad78ad2fe499dd16d42759f7a3801645399cce5c404415daa97" - ], - "markers": "python_version >= '3.5'", - "version": "==0.3.0" - }, - "mypy": { - "hashes": [ - "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9", - "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a", - "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9", - "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e", - "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2", - "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212", - "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b", - "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885", - "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150", - "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703", - "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072", - "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457", - "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e", - "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0", - "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb", - "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97", - "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8", - "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811", - "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6", - "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de", - "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504", - "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921", - "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d" - ], - "index": "pypi", - "version": "==0.910" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "pip": { - "hashes": [ - "sha256:deaf32dcd9ab821e359cd8330786bcd077604b5c5730c0b096eda46f95c24a2d", - "sha256:fd11ba3d0fdb4c07fbc5ecbba0b1b719809420f25038f8ee3cd913d3faa3033a" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3.1" - }, - "pipenv": { - "hashes": [ - "sha256:3b80b4512934b9d8e8ce12c988394642ff96bb697680e5b092e59af503178327", - "sha256:f84d7119239b22ab2ac2b8fbc7d619d83cf41135206d72a17c4f151cda529fd0" - ], - "index": "pypi", - "version": "==2022.1.8" - }, - "platformdirs": { - "hashes": [ - "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", - "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" - ], - "markers": "python_version >= '3.7'", - "version": "==2.4.1" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "psutil": { - "hashes": [ - "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5", - "sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a", - "sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4", - "sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841", - "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d", - "sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d", - "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0", - "sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845", - "sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf", - "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b", - "sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07", - "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618", - "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2", - "sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd", - "sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666", - "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce", - "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3", - "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d", - "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25", - "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492", - "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b", - "sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d", - "sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2", - "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203", - "sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2", - "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94", - "sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9", - "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64", - "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56", - "sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3", - "sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c", - "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==5.9.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pyparsing": { - "hashes": [ - "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", - "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.6" - }, - "requests": { - "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.27.1" - }, - "sanitized-package": { - "editable": true, - "path": "." - }, - "setuptools": { - "hashes": [ - "sha256:2404879cda71495fc4d5cbc445ed52fdaddf352b36e40be8dcc63147cb4edabe", - "sha256:68eb94073fc486091447fcb0501efd6560a0e5a1839ba249e5ff3c4c93f05f90" - ], - "markers": "python_version >= '3.7'", - "version": "==60.5.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "tox": { - "hashes": [ - "sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993", - "sha256:be3362472a33094bce26727f5f771ca0facf6dafa217f65875314e9a6600c95c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.24.5" - }, - "tox-pipenv": { - "hashes": [ - "sha256:5fe576294be7a5a14ba4bdea729d9c738e6c7d423ab84273c9b106d5b2508999", - "sha256:f51476491b52c22455fe37f31aa1a0c5aa9798d223f19be58dfadcf79e503362" - ], - "index": "pypi", - "version": "==1.10.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "urllib3": { - "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" - }, - "virtualenv": { - "hashes": [ - "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09", - "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.13.0" - }, - "virtualenv-clone": { - "hashes": [ - "sha256:418ee935c36152f8f153c79824bb93eaf6f0f7984bae31d3f48f350b9183501a", - "sha256:44d5263bceed0bac3e1424d64f798095233b64def1c5689afa43dc3223caf5b0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.5.7" - }, - "websocket-client": { - "hashes": [ - "sha256:1315816c0acc508997eb3ae03b9d3ff619c9d12d544c9a9b553704b1cc4f6af5", - "sha256:2eed4cc58e4d65613ed6114af2f380f7910ff416fc8c46947f6e76b6815f56c0" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.3" - } - }, - "develop": { - "alabaster": { - "hashes": [ - "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", - "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" - ], - "version": "==0.7.12" - }, - "attrs": { - "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" - }, - "babel": { - "hashes": [ - "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", - "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.1" - }, - "bleach": { - "hashes": [ - "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", - "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" - ], - "markers": "python_version >= '3.6'", - "version": "==4.1.0" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "cffi": { - "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", - "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" - ], - "markers": "python_version >= '3'", - "version": "==2.0.10" - }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.4" - }, - "coverage": { - "hashes": [ - "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", - "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", - "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", - "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", - "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", - "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", - "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", - "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", - "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", - "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", - "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", - "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", - "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", - "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", - "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", - "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", - "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", - "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", - "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", - "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", - "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", - "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", - "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", - "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", - "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", - "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", - "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", - "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", - "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", - "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", - "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", - "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", - "version": "==4.5.4" - }, - "coveralls": { - "hashes": [ - "sha256:25522a50cdf720d956601ca6ef480786e655ae2f0c94270c77e1a23d742de558", - "sha256:8e3315e8620bb6b3c6f3179a75f498e7179c93b3ddc440352404f941b1f70524" - ], - "index": "pypi", - "version": "==1.9.2" - }, - "cryptography": { - "hashes": [ - "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", - "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31", - "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac", - "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf", - "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316", - "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca", - "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638", - "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94", - "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12", - "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173", - "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b", - "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a", - "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f", - "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2", - "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9", - "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46", - "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903", - "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3", - "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1", - "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee" - ], - "markers": "python_version >= '3.6'", - "version": "==36.0.1" - }, - "distlib": { - "hashes": [ - "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b", - "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579" - ], - "version": "==0.3.4" - }, - "docopt": { - "hashes": [ - "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" - ], - "version": "==0.6.2" - }, - "docutils": { - "hashes": [ - "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", - "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.18.1" - }, - "filelock": { - "hashes": [ - "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80", - "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.2" - }, - "flake8": { - "hashes": [ - "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634", - "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5" - ], - "index": "pypi", - "version": "==3.8.2" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3'", - "version": "==3.3" - }, - "imagesize": { - "hashes": [ - "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c", - "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.0" - }, - "importlib-metadata": { - "hashes": [ - "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6", - "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4" - ], - "markers": "python_version >= '3.7'", - "version": "==4.10.0" - }, - "jedi": { - "hashes": [ - "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d", - "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab" - ], - "markers": "python_version >= '3.6'", - "version": "==0.18.1" - }, - "jeepney": { - "hashes": [ - "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac", - "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.7.1" - }, - "jinja2": { - "hashes": [ - "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", - "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.3" - }, - "keyring": { - "hashes": [ - "sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9", - "sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261" - ], - "markers": "python_version >= '3.7'", - "version": "==23.5.0" - }, - "markupsafe": { - "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", - "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", - "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", - "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "more-itertools": { - "hashes": [ - "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b", - "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064" - ], - "markers": "python_version >= '3.5'", - "version": "==8.12.0" - }, - "mypy": { - "hashes": [ - "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9", - "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a", - "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9", - "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e", - "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2", - "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212", - "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b", - "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885", - "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150", - "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703", - "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072", - "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457", - "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e", - "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0", - "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb", - "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97", - "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8", - "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811", - "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6", - "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de", - "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504", - "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921", - "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d" - ], - "index": "pypi", - "version": "==0.910" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "parso": { - "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" - ], - "markers": "python_version >= '3.6'", - "version": "==0.8.3" - }, - "pkginfo": { - "hashes": [ - "sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff", - "sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc" - ], - "version": "==1.8.2" - }, - "platformdirs": { - "hashes": [ - "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", - "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" - ], - "markers": "python_version >= '3.7'", - "version": "==2.4.1" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", - "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", - "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db" - ], - "version": "==2.0.10" - }, - "ptpython": { - "hashes": [ - "sha256:0977f56c934789d9955839ef71268148858f826fda49b267cb377ac7501a897b", - "sha256:90e24040e82de4abae0bbe6e352d59ae6657e14e1154e742c0038679361b052f" - ], - "index": "pypi", - "version": "==2.0.6" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", - "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.6.0" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.21" - }, - "pyflakes": { - "hashes": [ - "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", - "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.2.0" - }, - "pygments": { - "hashes": [ - "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65", - "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a" - ], - "markers": "python_version >= '3.5'", - "version": "==2.11.2" - }, - "pyparsing": { - "hashes": [ - "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", - "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.6" - }, - "pytest": { - "hashes": [ - "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3", - "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698" - ], - "index": "pypi", - "version": "==5.4.2" - }, - "pytest-cov": { - "hashes": [ - "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", - "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" - ], - "index": "pypi", - "version": "==2.8.1" - }, - "pytz": { - "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" - ], - "version": "==2021.3" - }, - "readme-renderer": { - "hashes": [ - "sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d", - "sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85" - ], - "markers": "python_version >= '3.6'", - "version": "==32.0" - }, - "requests": { - "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.27.1" - }, - "requests-toolbelt": { - "hashes": [ - "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", - "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" - ], - "version": "==0.9.1" - }, - "rfc3986": { - "hashes": [ - "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", - "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "secretstorage": { - "hashes": [ - "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", - "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.1" - }, - "setuptools": { - "hashes": [ - "sha256:2404879cda71495fc4d5cbc445ed52fdaddf352b36e40be8dcc63147cb4edabe", - "sha256:68eb94073fc486091447fcb0501efd6560a0e5a1839ba249e5ff3c4c93f05f90" - ], - "markers": "python_version >= '3.7'", - "version": "==60.5.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "snowballstemmer": { - "hashes": [ - "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", - "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" - ], - "version": "==2.2.0" - }, - "sphinx": { - "hashes": [ - "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66", - "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb" - ], - "index": "pypi", - "version": "==2.4.4" - }, - "sphinx-autodoc-typehints": { - "hashes": [ - "sha256:27c9e6ef4f4451766ab8d08b2d8520933b97beb21c913f3df9ab2e59b56e6c6c", - "sha256:a6b3180167479aca2c4d1ed3b5cb044a70a76cccd6b38662d39288ebd9f0dff0" - ], - "index": "pypi", - "version": "==1.10.3" - }, - "sphinxcontrib-applehelp": { - "hashes": [ - "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", - "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.2" - }, - "sphinxcontrib-devhelp": { - "hashes": [ - "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", - "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.2" - }, - "sphinxcontrib-htmlhelp": { - "hashes": [ - "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", - "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.0" - }, - "sphinxcontrib-jsmath": { - "hashes": [ - "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", - "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.1" - }, - "sphinxcontrib-qthelp": { - "hashes": [ - "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", - "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.3" - }, - "sphinxcontrib-serializinghtml": { - "hashes": [ - "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", - "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" - ], - "markers": "python_version >= '3.5'", - "version": "==1.1.5" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "tox": { - "hashes": [ - "sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993", - "sha256:be3362472a33094bce26727f5f771ca0facf6dafa217f65875314e9a6600c95c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.24.5" - }, - "tqdm": { - "hashes": [ - "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c", - "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.62.3" - }, - "twine": { - "hashes": [ - "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218", - "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936" - ], - "index": "pypi", - "version": "==3.4.2" - }, - "typing-extensions": { - "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "urllib3": { - "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" - }, - "virtualenv": { - "hashes": [ - "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09", - "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.13.0" - }, - "wcwidth": { - "hashes": [ - "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", - "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" - ], - "version": "==0.2.5" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "zipp": { - "hashes": [ - "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", - "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" - ], - "markers": "python_version >= '3.7'", - "version": "==3.7.0" - } - } -} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..732120d --- /dev/null +++ b/poetry.lock @@ -0,0 +1,501 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +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]"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "docker" +version = "7.0.0" +description = "A Python library for the Docker Engine API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "docker-7.0.0-py3-none-any.whl", hash = "sha256:12ba681f2777a0ad28ffbcc846a69c31b4dfd9752b47eb425a274ee269c5e14b"}, + {file = "docker-7.0.0.tar.gz", hash = "sha256:323736fb92cd9418fc5e7133bc953e11a9da04f4483f828b527db553f1e7e5a3"}, +] + +[package.dependencies] +packaging = ">=14.0" +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" + +[package.extras] +ssh = ["paramiko (>=2.4.3)"] +websockets = ["websocket-client (>=1.3.0)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "mslex" +version = "1.2.0" +description = "shlex for windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mslex-1.2.0-py3-none-any.whl", hash = "sha256:c68ec637485ee3544c5847c1b4e78b02940b32708568fb1d8715491815aa2341"}, + {file = "mslex-1.2.0.tar.gz", hash = "sha256:79e2abc5a129dd71cdde58a22a2039abb7fa8afcbac498b723ba6e9b9fbacc14"}, +] + +[[package]] +name = "mypy" +version = "1.10.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.4.4" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, + {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, + {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, + {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, + {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, + {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, +] + +[[package]] +name = "types-docker" +version = "7.0.0.20240519" +description = "Typing stubs for docker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-docker-7.0.0.20240519.tar.gz", hash = "sha256:d3741a3c21d3300dfcec206abd06c2315152871758e1553ed18b21ad0c33ded5"}, + {file = "types_docker-7.0.0.20240519-py3-none-any.whl", hash = "sha256:c26a4f8c19557a889bf2604de5032d19aaf8984fc71a3abaf95dd906ef2c455f"}, +] + +[package.dependencies] +types-requests = "*" +urllib3 = ">=2" + +[[package]] +name = "types-psutil" +version = "5.9.5.20240516" +description = "Typing stubs for psutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-psutil-5.9.5.20240516.tar.gz", hash = "sha256:bb296f59fc56458891d0feb1994717e548a1bcf89936a2877df8792b822b4696"}, + {file = "types_psutil-5.9.5.20240516-py3-none-any.whl", hash = "sha256:83146ded949a10167d9895e567b3b71e53ebc5e23fd8363eab62b3c76cce7b89"}, +] + +[[package]] +name = "types-requests" +version = "2.31.0.20240406" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, + {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.11,<4" +content-hash = "d758644ed364bcc7f551310d2ac79e599ca3fa36eb4386f6396baf505d14b523" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a81b0f3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,103 @@ +[tool.poetry] +name = "dockerblade" +version = "0.6.0" +description = "Interact with Docker containers via Python-like APIs" +authors = ["Chris Timperley "] +license = "Apache-2.0" +repository = "https://github.com/ChrisTimperley/dockerblade" +readme = "README.rst" +classifiers = [ + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[tool.poetry.dependencies] +python = ">=3.11,<4" +loguru = ">=0.5" +attrs = ">=19.2.0" +docker = "^7.0" +psutil = "^5.7" +requests = "2.31.0" +mslex = "^1.2" + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +mypy = ">=1.10.0" +pytest = "^7.4.3" +ruff = ">=0.4.4" +types-psutil = "^5.9.5.20240516" +types-docker = "^7.0.0.20240519" + +[tool.mypy] +ignore_missing_imports = false +strict = true +untyped_calls_exclude = [ + "docker" +] + +[tool.ruff] +line-length = 120 +target-version = "py311" +src = ["src"] + +[tool.ruff.lint] +select = [ + "ALL", # includes all rules (including new ones) +] +per-file-ignores = {"*/__init__.py" = ["F401"]} +extend-ignore = [ + "ANN101", # missing type annotation for 'self' in method + "ANN102", # missing type annotation for 'cls' in classmethod + "B024", # use of abstract base class without abstract methods + "D100", # missing docstring in public module + "D101", # missing docstring in public class + "D102", # missing docstring in public method + "D103", # missing docstring in public function + "D104", # missing docstring in public package + "D105", # missing docstring in magic method + "D203", # one-blank-line-before-class + "D213", # multi-line-summary-second-line + "D401", # first line of docstring should be in imperative mood + "D413", # missing blank line after last section + "FIX001", # unresolved FIXME + "FIX002", # TODO found + "TD001", # invalid TODO tag: `FIXME` + "TD002", # missing author in TODO + "TD003", # missing issue link on line following TODO + "TD004", # missing colon in TODO + "TRY003", # avoid specifying long messages outside the exception class + "S101", # use of assertions + "SLF001", # private member accessed + "T201", # use of print + "D204", # 1 blank line required after class docstring + "C901", # function is too complex (cyclomatic complexity) + "PLR0912", # too many branches + "PGH003", # use specific rule codes when ignoring type issues + "FBT001", # boolean typed positional argument in function definition + "ARG001", # unused function argument + "PLR0913", # too many arguments in function definition + "PLR0911", # too many return statements + "SIM108", # use ternary operator + "N818", # exception names should include an "Error" suffix + "PGH004", # use specific codes when using noqa + "S108", # probable insecure usage of temporary file or directory + "S602", # security risk: use of shell=True + "PTH118", # `os.path.join()` should be replaced by `Path` with `/` operator + "PTH123", # `open()` should be replaced by `Path.open()` + "PTH120", # `os.path.dirname()` should be replaced by `Path.parent` + "PTH112", # `os.path.isdir()` should be replaced by `Path.is_dir()` + "PTH110", # `os.path.exists()` should be replaced by `Path.exists()` + "FBT002", # boolean default positional argument in function definition + "A001", # variable X is shadowing a Python builtin +] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index f1c22df..0000000 --- a/setup.cfg +++ /dev/null @@ -1,78 +0,0 @@ -[metadata] -name = dockerblade -author = Christopher Timperley -author-email = christimperley@googlemail.com -description-file = README.rst -license-file = LICENSE -url = https://github.com/ChrisTimperley/dockerblade -keywords = docker, python, files, shell, wrapper -classifiers = - Natural Language :: English - Intended Audience :: Developers - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - -[tox:tox] -envlist = py36, py37, py38, py39 - -[testenv] -#deps = -# flake8==3.8.2 -# pep8-naming==0.10.0 -# mypy==0.770 -# pytest==5.4.2 -# pytest-cov==2.10.0 -commands = - mypy src - flake8 src - pytest --cov=dockerblade - -[aliases] -docs = build_sphinx -test = pytest - -[tool:pytest] -addopts = -rx -v - -[mypy] -ignore_missing_imports = True -disallow_untyped_defs = True - -[flake8] -ignore = W605,E302 -max-line-length = 79 -per-file-ignores = - src/dockerblade/__init__.py:F401 - src/dockerblade/files.py:F811 - src/dockerblade/shell.py:F811 - -[build_sphinx] -source_dir = docs -build_dir = docs/_build - -[options] -include_package_data = True -python_requires = >= 3.6 -install_requires = - attrs >= 19.2.0 - docker ~= 4.1 - loguru >= 0.4 - psutil ~= 5.7 - mslex == 0.3 -package_dir = - =src -packages = find: - -[options.packages.find] -where = src - -[options.extras_require] -test = - pytest ~= 5.2.1 -build_sphinx = - sphinx ~= 2.4.4 - sphinx-autodoc-typehints ~= 1.10.3 diff --git a/setup.py b/setup.py deleted file mode 100644 index eacd007..0000000 --- a/setup.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import setuptools - -PACKAGE = 'dockerblade' -PATH = os.path.join(os.path.dirname(__file__), - 'src/{}/version.py'.format(PACKAGE)) -with open(PATH, 'r') as fh: - exec(fh.read()) - -setuptools.setup(version=__version__) diff --git a/src/dockerblade/__init__.py b/src/dockerblade/__init__.py index df7e9ba..7315ffa 100644 --- a/src/dockerblade/__init__.py +++ b/src/dockerblade/__init__.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- from loguru import logger as _logger from . import exceptions from .container import Container from .daemon import DockerDaemon from .files import FileSystem -from .shell import Shell, CompletedProcess, CalledProcessError +from .shell import CalledProcessError, CompletedProcess, Shell from .stopwatch import Stopwatch -_logger.disable('dockerblade') +_logger.disable("dockerblade") diff --git a/src/dockerblade/container.py b/src/dockerblade/container.py index 98d2e80..07cbe4a 100644 --- a/src/dockerblade/container.py +++ b/src/dockerblade/container.py @@ -1,20 +1,25 @@ -# -*- coding: utf-8 -*- -__all__ = ('Container',) +from __future__ import annotations -from types import TracebackType -import typing -from typing import Any, Mapping, Optional, Sequence, Type +__all__ = ("Container",) + +import typing as t import attr -from docker.models.containers import Container as DockerContainer -from .shell import Shell from .files import FileSystem +from .shell import Shell + +if t.TYPE_CHECKING: + from collections.abc import Mapping, Sequence + from types import TracebackType + from typing import Any + + from docker.models.containers import Container as DockerContainer -if typing.TYPE_CHECKING: from .daemon import DockerDaemon + @attr.s(slots=True, frozen=True) class Container: """Provides access to a Docker container. @@ -30,52 +35,62 @@ class Container: name: str, optional The name, if any, assigned to the container. """ - daemon: 'DockerDaemon' = attr.ib() + daemon: DockerDaemon = attr.ib() _docker: DockerContainer = \ attr.ib(repr=False, eq=False, hash=False) id: str = attr.ib(init=False, repr=True) - name: Optional[str] = attr.ib(init=False, repr=False) + name: str | None = attr.ib(init=False, repr=False) pid: int = attr.ib(init=False, repr=False) def __attrs_post_init__(self) -> None: - object.__setattr__(self, 'id', self._docker.id) - object.__setattr__(self, 'name', self._docker.name) - object.__setattr__(self, 'pid', int(self._info['State']['Pid'])) + object.__setattr__(self, "id", self._docker.id) + object.__setattr__(self, "name", self._docker.name) + object.__setattr__(self, "pid", int(self._info["State"]["Pid"])) @property def _info(self) -> Mapping[str, Any]: """Retrieves information about this container from Docker.""" - return self.daemon.api.inspect_container(self.id) + info: dict[str, Any] = self.daemon.api.inspect_container(self.id) + assert isinstance(info, dict) + return info def _exec_id_to_host_pid(self, exec_id: str) -> int: """Returns the host PID for a given exec command in this container.""" - return self.daemon.api.exec_inspect(exec_id)['Pid'] + pid: int = self.daemon.api.exec_inspect(exec_id)["Pid"] + assert isinstance(pid, int) + return pid def shell(self, - path: str = '/bin/sh', + path: str = "/bin/sh", *, - sources: Optional[Sequence[str]] = None, - environment: Optional[Mapping[str, str]] = None + sources: Sequence[str] | None = None, + environment: Mapping[str, str] | None = None, ) -> Shell: """Constructs a shell for this Docker container.""" if not environment: environment = {} if not sources: - sources = tuple() - return Shell(self, path, sources=sources, environment=environment) + sources = () + return Shell( + self, + path, + sources=sources, + environment=environment, + ) def filesystem(self) -> FileSystem: """Provides access to the filesystem for this container.""" return FileSystem(self, self.shell()) - def remove(self, force: bool = True) -> None: + def remove(self, *, force: bool = True) -> None: """Removes this Docker container.""" self._docker.remove(force=force) - def persist(self, - repository: Optional[str] = None, - tag: Optional[str] = None - ) -> str: + def persist( + self, + repository: str | None = None, + tag: str | None = None, + ) -> str: """Persists this container to an image. Parameters @@ -90,31 +105,36 @@ def persist(self, str The ID of the generated image. """ - return self._docker.commit(repository, tag).id + id_: str = self._docker.commit(repository, tag).id + assert isinstance(id_, str) + return id_ @property - def ip_address(self) -> Optional[str]: - """The local IP address, if any, assigned to this container. If the - container uses the host network mode, 127.0.0.1 (i.e., localhost) will - be assigned as the ip_address of this container. + def ip_address(self) -> str | None: + """The local IP address, if any, assigned to this container. + + If the container uses the host network mode, 127.0.0.1 (i.e., localhost) + will be assigned as the ip_address of this container. """ info = self._info - if info['HostConfig']['NetworkMode'] == 'host': - return '127.0.0.1' - else: - return info['NetworkSettings'].get('IPAddress', None) + if info["HostConfig"]["NetworkMode"] == "host": + return "127.0.0.1" + ip_address: str | None = info["NetworkSettings"].get("IPAddress", None) + return ip_address @property def network_mode(self) -> str: """The network mode used by this container.""" - return self._info['HostConfig']['NetworkMode'] + mode: str = self._info["HostConfig"]["NetworkMode"] + assert isinstance(mode, str) + return mode - def __enter__(self) -> 'Container': + def __enter__(self) -> t.Self: return self def __exit__(self, - ex_type: Optional[Type[BaseException]], - ex_val: Optional[BaseException], - ex_tb: Optional[TracebackType] + ex_type: type[BaseException] | None, + ex_val: BaseException | None, + ex_tb: TracebackType | None, ) -> None: self.remove() diff --git a/src/dockerblade/daemon.py b/src/dockerblade/daemon.py index a62415e..3fd9d91 100644 --- a/src/dockerblade/daemon.py +++ b/src/dockerblade/daemon.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- -__all__ = ('DockerDaemon',) +__all__ = ("DockerDaemon",) +from collections.abc import Mapping from types import TracebackType -from typing import Any, Mapping, Optional, Type +from typing import Any -from loguru import logger import attr import docker +from loguru import logger from .container import Container @@ -14,7 +14,7 @@ @attr.s(frozen=True) class DockerDaemon: """Maintains a connection to a Docker daemon.""" - url: Optional[str] = attr.ib(default=None) + url: str | None = attr.ib(default=None) client: docker.DockerClient = \ attr.ib(init=False, eq=False, hash=False, repr=False) api: docker.APIClient = \ @@ -23,17 +23,17 @@ class DockerDaemon: def __attrs_post_init__(self) -> None: client = docker.DockerClient(self.url) api = client.api - object.__setattr__(self, 'client', client) - object.__setattr__(self, 'api', api) + object.__setattr__(self, "client", client) + object.__setattr__(self, "api", api) logger.debug(f"created daemon connection: {self}") - def __enter__(self) -> 'DockerDaemon': + def __enter__(self) -> "DockerDaemon": return self def __exit__(self, - ex_type: Optional[Type[BaseException]], - ex_val: Optional[BaseException], - ex_tb: Optional[TracebackType] + ex_type: type[BaseException] | None, + ex_val: BaseException | None, + ex_tb: TracebackType | None, ) -> None: self.close() @@ -53,19 +53,19 @@ def attach(self, id_or_name: str) -> Container: def provision(self, image: str, - command: Optional[str] = None, + command: str | None = None, *, - entrypoint: Optional[str] = None, - environment: Optional[Mapping[str, str]] = None, - network_mode: str = 'bridge', - name: Optional[str] = None, - ports: Optional[Mapping[int, int]] = None, - user: Optional[str] = None, - volumes: Optional[Mapping[str, Any]] = None, + entrypoint: str | None = None, + environment: Mapping[str, str] | None = None, + network_mode: str = "bridge", + name: str | None = None, + ports: Mapping[int, int] | None = None, + user: str | None = None, + volumes: Mapping[str, Any] | None = None, ) -> Container: """Creates a Docker container from a given image. - Arguments + Arguments: --------- image: str The name of the Docker image that should be used. @@ -101,7 +101,7 @@ def provision(self, container. Can be either :code:`bridge`, :code`none`, :code:`container:`, or :code:`host`. - Returns + Returns: ------- Container An interface to the newly launched container. diff --git a/src/dockerblade/exceptions.py b/src/dockerblade/exceptions.py index f299376..19c08cf 100644 --- a/src/dockerblade/exceptions.py +++ b/src/dockerblade/exceptions.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- +import subprocess as _subprocess import typing as _t + import attr as _attr -import subprocess as _subprocess class DockerBladeException(Exception): @@ -12,12 +12,12 @@ class DockerBladeException(Exception): class UnexpectedError(DockerBladeException): """An unexpected error occurred during an operation.""" description: str - error: _t.Optional['CalledProcessError'] = _attr.ib(default=None) + error: _t.Optional["CalledProcessError"] = _attr.ib(default=None) def __str__(self) -> str: msg = f"An unexpected error occurred: {self.description}" if self.error: - msg += f' ({self.error})' + msg += f" ({self.error})" return msg @@ -36,7 +36,7 @@ class CopyFailed(DockerBladeException): reason: str def __str__(self) -> str: - return f'Copy operation failed: {self.reason}' + return f"Copy operation failed: {self.reason}" @_attr.s(frozen=True, auto_exc=True, auto_attribs=True) @@ -45,7 +45,7 @@ class IsADirectoryError(DockerBladeException): path: str def __str__(self) -> str: - return f'Directory exists at path where file is expected: {self.path}' + return f"Directory exists at path where file is expected: {self.path}" @_attr.s(frozen=True, auto_exc=True, auto_attribs=True) @@ -54,7 +54,7 @@ class DirectoryNotEmpty(DockerBladeException): path: str def __str__(self) -> str: - return f'Directory is not empty: {self.path}' + return f"Directory is not empty: {self.path}" @_attr.s(frozen=True, auto_exc=True, auto_attribs=True) @@ -63,7 +63,7 @@ class IsNotADirectoryError(DockerBladeException): path: str def __str__(self) -> str: - return f'Directory was expected at path: {self.path}' + return f"Directory was expected at path: {self.path}" @_attr.s(frozen=True, auto_exc=True, auto_attribs=True) @@ -72,7 +72,7 @@ class HostFileNotFound(DockerBladeException): path: str def __str__(self) -> str: - return f'File not found [{self.path}] on host machine' + return f"File not found [{self.path}] on host machine" @_attr.s(frozen=True, auto_exc=True, auto_attribs=True) @@ -82,8 +82,8 @@ class ContainerFileNotFound(DockerBladeException): path: str def __str__(self) -> str: - return (f'File not found [{self.path}] ' - f'in container [{self.container_id}]') + return (f"File not found [{self.path}] " + f"in container [{self.container_id}]") @_attr.s(frozen=True, auto_exc=True, auto_attribs=True) @@ -93,8 +93,8 @@ class ContainerFileAlreadyExists(DockerBladeException): path: str def __str__(self) -> str: - return (f'File already exists [{self.path}] ' - f'in container [{self.container_id}]') + return (f"File already exists [{self.path}] " + f"in container [{self.container_id}]") @_attr.s(frozen=True, auto_exc=True, auto_attribs=True) @@ -115,7 +115,7 @@ class CalledProcessError(DockerBladeException, _subprocess.CalledProcessError): cmd: str returncode: int duration: float - output: _t.Optional[_t.Union[str, bytes]] + output: str | bytes | None @_attr.s(frozen=True, auto_exc=True, auto_attribs=True) diff --git a/src/dockerblade/files.py b/src/dockerblade/files.py index cafa396..92ca2d1 100644 --- a/src/dockerblade/files.py +++ b/src/dockerblade/files.py @@ -1,25 +1,29 @@ -# -*- coding: utf-8 -*- -__all__ = ('FileSystem',) +__all__ = ("FileSystem",) -from typing import Iterator, Union, List, Optional, overload -from typing_extensions import Literal import contextlib -import typing -import subprocess import os +import subprocess +import tempfile +import typing +from collections.abc import Iterator +from pathlib import Path +from typing import Literal, overload import attr -import tempfile from loguru import logger from . import exceptions as exc from .util import quote_container, quote_host if typing.TYPE_CHECKING: - from .shell import Shell from .container import Container + from .shell import Shell + +_ON_WINDOWS = os.name == "nt" -_ON_WINDOWS = os.name == 'nt' +EXIT_CODE_FILE_NOT_FOUND = 50 +EXIT_CODE_IS_NOT_A_DIRECTORY = 51 +EXIT_CODE_FILE_ALREADY_EXISTS = 49 @attr.s(slots=True) @@ -31,18 +35,17 @@ class FileSystem: container: Container The container to which this filesystem belongs. """ - container: 'Container' = attr.ib() - _shell: 'Shell' = attr.ib(repr=False, eq=False, hash=False) + container: "Container" = attr.ib() + _shell: "Shell" = attr.ib(repr=False, eq=False, hash=False) def copy_from_host(self, path_host: str, path_container: str) -> None: - """ - Copies a given file or directory tree from the host to the container. + """Copies a given file or directory tree from the host to the container. Parameters ---------- - fn_host: str + path_host: str the file or directory tree that should be copied from the host. - fn_container: str + path_container: str the destination path on the container. Raises @@ -72,23 +75,24 @@ def copy_from_host(self, path_host: str, path_container: str) -> None: f"{id_container}:{path_container_escaped}") try: subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError: + except subprocess.CalledProcessError as error: path_container_parent: str = os.path.dirname(path_container) if not self.isdir(path_container_parent): - raise exc.ContainerFileNotFound(path=path_container_parent, - container_id=id_container) + raise exc.ContainerFileNotFound( + path=path_container_parent, + container_id=id_container, + ) from error reason = (f"failed to copy file [{path_host}] " f"from host to container [{id_container}]: " f"{path_container}") - raise exc.CopyFailed(reason) + raise exc.CopyFailed(reason) from error def copy_to_host(self, path_container: str, - path_host: str + path_host: str, ) -> None: - """ - Copies a given file or directory tree from the container to the host. + """Copies a given file or directory tree from the container to the host. Parameters ---------- @@ -126,25 +130,28 @@ def copy_to_host(self, f"{path_host_escaped}") try: subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError: + except subprocess.CalledProcessError as error: if not self.exists(path_container): - raise exc.ContainerFileNotFound(path=path_container, - container_id=id_container) + raise exc.ContainerFileNotFound( + path=path_container, + container_id=id_container, + ) from error reason = (f"failed to copy file [{path_container}] " f"from container [{id_container}] to host: " f"{path_host}") - raise exc.CopyFailed(reason) + raise exc.CopyFailed(reason) from error def remove(self, filename: str) -> None: """Removes a given file. + Inspired by :meth:`os.remove`. - Warning + Warning: ------- Does not handle permissions errors. - Raises + Raises: ------ ContainerFileNotFound if the given file does not exist. @@ -153,27 +160,37 @@ def remove(self, filename: str) -> None: UnexpectedError if an unexpected failure occurs. """ - command = f'rm {quote_container(filename)}' + command = f"rm {quote_container(filename)}" try: self._shell.check_call(command) except exc.CalledProcessError as error: if not self.exists(filename): - raise exc.ContainerFileNotFound(path=filename, - container_id=self.container.id) - elif self.isdir(filename): - raise exc.IsADirectoryError(path=filename) + raise exc.ContainerFileNotFound( + path=filename, + container_id=self.container.id, + ) from error + + if self.isdir(filename): + raise exc.IsADirectoryError( + path=filename, + ) from error - raise exc.UnexpectedError('failed to remove', error) from error + error_message = "failed to remove file" + raise exc.UnexpectedError( + error_message, + error, + ) from error def rmdir(self, directory: str) -> None: """Removes a given directory. + Inspired by :meth:`os.rmdir`. - Warning + Warning: ------- Does not handle permissions errors. - Raises + Raises: ------ ContainerFileNotFound if the given directory does not exist. @@ -185,22 +202,28 @@ def rmdir(self, directory: str) -> None: an unexpected failure occurred. """ escaped_directory = quote_container(directory) - command = (f'test -e {escaped_directory} || exit 50 && ' - f'test -d {escaped_directory} || exit 51 && ' - f'rmdir {escaped_directory}') + command = (f"test -e {escaped_directory} || exit 50 && " + f"test -d {escaped_directory} || exit 51 && " + f"rmdir {escaped_directory}") try: self._shell.check_output(command, text=True) except exc.CalledProcessError as error: - if error.returncode == 50: - raise exc.ContainerFileNotFound(path=directory, - container_id=self.container.id) - if error.returncode == 51: - raise exc.IsNotADirectoryError(path=directory) - if error.output and 'Directory not empty' in error.output: - raise exc.DirectoryNotEmpty(path=directory) from error + if error.returncode == EXIT_CODE_FILE_NOT_FOUND: + raise exc.ContainerFileNotFound( + path=directory, + container_id=self.container.id, + ) from error + if error.returncode == EXIT_CODE_IS_NOT_A_DIRECTORY: + raise exc.IsNotADirectoryError( + path=directory, + ) from error + if error.output and "Directory not empty" in error.output: + raise exc.DirectoryNotEmpty( + path=directory, + ) from error raise exc.UnexpectedError('failed to remove directory', error) from error # noqa - def write(self, filename: str, contents: Union[str, bytes]) -> None: + def write(self, filename: str, contents: str | bytes) -> None: """Writes to a given file. Parameters @@ -210,7 +233,7 @@ def write(self, filename: str, contents: Union[str, bytes]) -> None: contents: Union[str, bytes] the text or binary contents of the file. """ - mode = 'wb' if isinstance(contents, bytes) else 'w' + mode = "wb" if isinstance(contents, bytes) else "w" directory = os.path.dirname(filename) if not self.isdir(directory): raise exc.ContainerFileNotFound(path=directory, @@ -218,7 +241,7 @@ def write(self, filename: str, contents: Union[str, bytes]) -> None: # write to a temporary file on the host and copy to container with tempfile.TemporaryDirectory() as host_temp_dir: - temp_filename = os.path.join(host_temp_dir, 'contents') + temp_filename = os.path.join(host_temp_dir, "contents") with open(temp_filename, mode) as fh: fh.write(contents) self.copy_from_host(temp_filename, filename) @@ -235,7 +258,7 @@ def read(self, filename: str, binary: Literal[True]) -> bytes: def read(self, filename: str, binary: Literal[False]) -> str: ... - def read(self, filename: str, binary: bool = False) -> Union[str, bytes]: + def read(self, filename: str, binary: bool = False) -> str | bytes: """Reads the contents of a given file. Parameters @@ -253,20 +276,19 @@ def read(self, filename: str, binary: bool = False) -> Union[str, bytes]: IsADirectoryError If :code:`filename` is a directory. """ - mode = 'rb' if binary else 'r' + mode = "rb" if binary else "r" if self.isdir(filename): raise exc.IsADirectoryError(filename) with tempfile.TemporaryDirectory() as host_temp_dir: - filename_host_temp = os.path.join(host_temp_dir, 'contents') + filename_host_temp = os.path.join(host_temp_dir, "contents") self.copy_to_host(filename, filename_host_temp) with open(filename_host_temp, mode) as f: - return f.read() + content: str | bytes = f.read() + return content - def find(self, path: str, filename: str) -> List[str]: - """ - Returns a list of files that match a provided filename in a given - directory, recursively. + def find(self, path: str, filename: str) -> list[str]: + """Returns a list of files that match a filename in a directory, recursively. Parameters ---------- @@ -291,26 +313,36 @@ def find(self, path: str, filename: str) -> List[str]: """ # TODO execute as root path_escaped = quote_container(path) - command = (f'test ! -e {path_escaped} && exit 50 || ' - f'test ! -d {path_escaped} && exit 51 || ' - f'find {path_escaped} -name {quote_container(filename)}') + command = ( + f"test ! -e {path_escaped} && exit {EXIT_CODE_FILE_NOT_FOUND} || " + f"test ! -d {path_escaped} && exit {EXIT_CODE_IS_NOT_A_DIRECTORY} || " + f"find {path_escaped} -name {quote_container(filename)}" + ) try: output = self._shell.check_output(command, text=True) except exc.CalledProcessError as error: - if error.returncode == 50: - raise exc.ContainerFileNotFound(path=path, - container_id=self.container.id) from error # noqa - if error.returncode == 51: - raise exc.IsNotADirectoryError(path=path) from error - raise exc.UnexpectedError('find failed', error=error) from error - - paths: List[str] = output.replace('\r', '').split('\n') + if error.returncode == EXIT_CODE_FILE_NOT_FOUND: + raise exc.ContainerFileNotFound( + path=path, + container_id=self.container.id, + ) from error + if error.returncode == EXIT_CODE_IS_NOT_A_DIRECTORY: + raise exc.IsNotADirectoryError( + path=path, + ) from error + + error_message = "find failed" + raise exc.UnexpectedError( + error_message, + error=error, + ) from error + + paths: list[str] = output.replace("\r", "").split("\n") return paths - def makedirs(self, d: str, exist_ok: bool = False) -> None: - """ - Recursively creates a directory at a given path, creating any missing - intermediate directories along the way. + def makedirs(self, d: str, *, exist_ok: bool = False) -> None: + """Recursively creates a directory at a given path, creating any missing intermediate directories along the way. + Inspired by :meth:`os.makedirs`. Parameters @@ -332,25 +364,27 @@ def makedirs(self, d: str, exist_ok: bool = False) -> None: d_parent = os.path.dirname(d) if self.isdir(d) and not exist_ok: raise exc.ContainerFileAlreadyExists(path=d, - container_id=self.container.id) # noqa + container_id=self.container.id) if self.isfile(d): raise exc.ContainerFileAlreadyExists(path=d, - container_id=self.container.id) # noqa + container_id=self.container.id) if self.exists(d_parent) and self.isfile(d_parent): raise exc.IsNotADirectoryError(d_parent) - command = f'mkdir -p {quote_container(d)}' + command = f"mkdir -p {quote_container(d)}" self._shell.check_call(command) def exists(self, path: str) -> bool: """Determines whether a file or directory exists at the given path. + Inspired by :meth:`os.path.exists`. """ - cmd = f'test -e {quote_container(path)}' + cmd = f"test -e {quote_container(path)}" return self._shell.run(cmd, stdout=False).returncode == 0 def mkdir(self, directory: str) -> None: """Creates a directory at a given path. + Inspired by :meth:`os.mkdir`. Raises @@ -367,32 +401,40 @@ def mkdir(self, directory: str) -> None: directory_escaped = quote_container(directory) directory_parent = os.path.dirname(directory) directory_parent_escaped = quote_container(directory_parent) - command = (f"test -e {directory_escaped} && exit 50 || " - f"test ! -e {directory_parent_escaped} && exit 51 || " - f"test ! -d {directory_parent_escaped} && exit 52 || " - f"mkdir {directory_escaped}") + command = ( + f"test -e {directory_escaped} && exit {EXIT_CODE_FILE_ALREADY_EXISTS} || " + f"test ! -e {directory_parent_escaped} && exit {EXIT_CODE_FILE_NOT_FOUND} || " + f"test ! -d {directory_parent_escaped} && exit {EXIT_CODE_IS_NOT_A_DIRECTORY} || " + f"mkdir {directory_escaped}" + ) try: self._shell.check_call(command) except exc.CalledProcessError as error: - if error.returncode == 50: + if error.returncode == EXIT_CODE_FILE_ALREADY_EXISTS: raise exc.ContainerFileAlreadyExists( path=directory, container_id=self.container.id) from error - if error.returncode == 51: + if error.returncode == EXIT_CODE_FILE_NOT_FOUND: raise exc.ContainerFileNotFound( path=directory_parent, container_id=self.container.id) from error - if error.returncode == 52: + if error.returncode == EXIT_CODE_IS_NOT_A_DIRECTORY: raise exc.IsNotADirectoryError(directory_parent) from error - raise exc.UnexpectedError('mkdir failed', error) from error + + error_message = "mkdir failed" + raise exc.UnexpectedError( + error_message, + error, + ) from error def listdir(self, directory: str, *, - absolute: bool = False - ) -> List[str]: + absolute: bool = False, + ) -> list[str]: """Returns a list of the files belonging to a given directory. + Inspired by :meth:`os.listdir`. Parameters @@ -413,48 +455,56 @@ def listdir(self, if an unexpected error occurred during execution of this command """ directory_escaped = quote_container(directory) - command = (f'test -e {directory_escaped} || exit 50 && ' - f'test -d {directory_escaped} || exit 51 && ' - f'ls --color=never -A -1 {directory_escaped}') + command = ( + f"test -e {directory_escaped} || exit {EXIT_CODE_FILE_NOT_FOUND} && " + f"test -d {directory_escaped} || exit {EXIT_CODE_IS_NOT_A_DIRECTORY} && " + f"ls --color=never -A -1 {directory_escaped}" + ) try: output = self._shell.check_output(command, text=True) except exc.CalledProcessError as error: - if error.returncode == 50: - raise exc.ContainerFileNotFound(path=directory, - container_id=self.container.id) # noqa - if error.returncode == 51: - raise exc.IsNotADirectoryError(path=directory) + if error.returncode == EXIT_CODE_FILE_NOT_FOUND: + raise exc.ContainerFileNotFound( + path=directory, + container_id=self.container.id, + ) from error + if error.returncode == EXIT_CODE_IS_NOT_A_DIRECTORY: + raise exc.IsNotADirectoryError( + path=directory, + ) from error raise - paths: List[str] = output.replace('\r', '').split('\n') + paths: list[str] = output.replace("\r", "").split("\n") if absolute: - paths = [os.path.join(directory, p) for p in paths] + paths = [os.path.join(directory, path) for path in paths] return paths def isfile(self, path: str) -> bool: """Determines whether a regular file exists at a given path. + Inspired by :meth:`os.path.isfile`. """ - cmd = f'test -f {quote_container(path)}' + cmd = f"test -f {quote_container(path)}" return self._shell.run(cmd, stdout=False).returncode == 0 def isdir(self, path: str) -> bool: """Determines whether a directory exists at a given path. + Inspired by :meth:`os.path.dir`. """ - cmd = f'test -d {quote_container(path)}' + cmd = f"test -d {quote_container(path)}" return self._shell.run(cmd, stdout=False).returncode == 0 def islink(self, path: str) -> bool: """Determines whether a symbolic link exists at a given path. + Inspired by :meth:`os.path.islink`. """ - cmd = f'test -h {quote_container(path)}' + cmd = f"test -h {quote_container(path)}" return self._shell.run(cmd, stdout=False).returncode == 0 def access(self, path: str, mode: int) -> bool: - """Determines whether the user for the shell has access to perform a - given operation (e.g., existence, read, write, execute) at a path. + """Determines whether the shell user can perform an operation (e.g., existence, read, write, execute). Parameters ---------- @@ -479,21 +529,20 @@ def access(self, path: str, mode: int) -> bool: if mode == os.F_OK: return self.exists(path) - commands: List[str] = [] + commands: list[str] = [] if mode & os.X_OK > 0: - commands.append(f'test -x {escaped_path}') + commands.append(f"test -x {escaped_path}") if mode & os.R_OK > 0: - commands.append(f'test -r {escaped_path}') + commands.append(f"test -r {escaped_path}") if mode & os.W_OK > 0: - commands.append(f'test -w {escaped_path}') + commands.append(f"test -w {escaped_path}") if not any(commands): return False - command = ' && '.join(commands) + command = " && ".join(commands) outcome = self._shell.run(command, stdout=False, stderr=False) - has_access = outcome.returncode == 0 - return has_access + return outcome.returncode == 0 def patch(self, context: str, diff: str) -> None: """Attempts to atomically apply a given patch to the filesystem. @@ -519,30 +568,33 @@ def patch(self, context: str, diff: str) -> None: ContainerFileNotFoundError If the given context is neither a file or directory. """ - if not os.path.isabs(context): - raise ValueError("context must be supplied as an absolute path") + context_path = Path(context) + if not context_path.is_absolute(): + error = f"context must be supplied as an absolute path: {context}" + raise ValueError(error) - with self.tempfile(suffix='.diff') as fn_diff: + with self.tempfile(suffix=".diff") as fn_diff: self.write(fn_diff, diff) safe_context = quote_container(context) safe_fn_diff = quote_container(fn_diff) if self.isdir(context): - cmd = f'patch -u -p0 -f -i {safe_fn_diff} -d {safe_context}' + cmd = f"patch -u -p0 -f -i {safe_fn_diff} -d {safe_context}" elif self.isfile(context): - cmd = f'patch -u -f -i {safe_fn_diff} {safe_context}' + cmd = f"patch -u -f -i {safe_fn_diff} {safe_context}" else: raise exc.ContainerFileNotFound(path=context, - container_id=self.container.id) # noqa + container_id=self.container.id) self._shell.check_call(cmd) def mktemp(self, - suffix: Optional[str] = None, - prefix: Optional[str] = None, - dirname: Optional[str] = None + suffix: str | None = None, + prefix: str | None = None, + dirname: str | None = None, ) -> str: """Creates a temporary file. + Inspired by :class:`tempfile.mktemp`. Parameters @@ -566,38 +618,38 @@ def mktemp(self, The absolute path of the temporary file. """ template = quote_container(f"{prefix if prefix else 'tmp'}.XXXXXXXXXX") - dirname = dirname if dirname else '/tmp' - cmd_parts = ('mktemp', template, '-p', quote_container(dirname)) + dirname = dirname if dirname else "/tmp" + cmd_parts = ("mktemp", template, "-p", quote_container(dirname)) if not self.isdir(dirname): raise exc.ContainerFileNotFound(path=dirname, container_id=self.container.id) - command = ' '.join(cmd_parts) + command = " ".join(cmd_parts) filename = self._shell.check_output(command, text=True) if suffix: original_filename = filename filename = original_filename + suffix - command = f'mv {original_filename} {filename}' + command = f"mv {original_filename} {filename}" self._shell.check_call(command) return filename @contextlib.contextmanager def tempfile(self, - suffix: Optional[str] = None, - prefix: Optional[str] = None, - dirname: Optional[str] = None + suffix: str | None = None, + prefix: str | None = None, + dirname: str | None = None, ) -> Iterator[str]: """Creates a temporary file within a context. Upon exiting the context, the temporary file will be destroyed. Inspired by :class:`tempfile.TemporaryFile`. - Note + Note: ---- Accepts the same arguments as :meth:`mktemp`. - Yields + Yields: ------ str The absolute path of the temporary file. @@ -607,4 +659,4 @@ def tempfile(self, try: self.remove(filename) except exc.ContainerFileNotFound: - logger.debug('temporary file already destroyed: %s', filename) + logger.debug("temporary file already destroyed: %s", filename) diff --git a/src/dockerblade/popen.py b/src/dockerblade/popen.py index eb8648f..1242ce2 100644 --- a/src/dockerblade/popen.py +++ b/src/dockerblade/popen.py @@ -1,34 +1,41 @@ -# -*- coding: utf-8 -*- -__all__ = ('Popen',) +from __future__ import annotations + +__all__ = ("Popen",) -from typing import Any, Dict, Iterator, Optional, Union -import time import signal +import time +import typing as t +from pathlib import Path +from typing import Any -from docker import APIClient as DockerAPIClient -from docker.models.containers import Container as DockerContainer -from loguru import logger import attr +from loguru import logger -from .stopwatch import Stopwatch from .exceptions import TimeoutExpired +from .stopwatch import Stopwatch + +if t.TYPE_CHECKING: + from collections.abc import Iterator + + from docker import APIClient as DockerAPIClient + from .container import Container -def host_pid_to_container_pid(pid_host: int) -> Optional[int]: - fn_proc = f'/proc/{pid_host}/status' - try: - with open(fn_proc, 'r') as fh: - for line in fh: - if line.startswith('NSpid:'): - return int(line.split()[2]) - return None - except FileNotFoundError: - return None + + +def host_pid_to_container_pid(pid_host: int) -> int | None: + fn_proc = Path(f"/proc/{pid_host}/status") + with fn_proc.open() as fh: + for line in fh: + if line.startswith("NSpid:"): + return int(line.split()[2]) + return None @attr.s(slots=True, eq=False, hash=False) class Popen: """Provides access to a process that is running inside a given shell. + Inspired by the :class:`subprocess.Popen` interface within the Python standard library. Unlike :class:`subprocess.Popen`, instances of this class should be generated by :meth:`ShellProxy.popen` rather than via @@ -56,20 +63,22 @@ class should be generated by :meth:`ShellProxy.popen` rather than via """ args: str = attr.ib() cwd: str = attr.ib() - _container: DockerContainer = attr.ib() + _container: Container = attr.ib() _docker_api: DockerAPIClient = attr.ib(repr=False) _exec_id: int = attr.ib() _stream: Iterator[bytes] = attr.ib(repr=False) - _pid: Optional[int] = attr.ib(init=False, default=None, repr=False) - _pid_host: Optional[int] = attr.ib(init=False, default=None) - _returncode: Optional[int] = attr.ib(init=False, default=None) - encoding: Optional[str] = attr.ib(default='utf-8') + _pid: int | None = attr.ib(init=False, default=None, repr=False) + _pid_host: int | None = attr.ib(init=False, default=None) + _returncode: int | None = attr.ib(init=False, default=None) + encoding: str | None = attr.ib(default="utf-8") - def _inspect(self) -> Dict[str, Any]: - return self._docker_api.exec_inspect(self._exec_id) + def _inspect(self) -> dict[str, Any]: + info: dict[str, Any] = self._docker_api.exec_inspect(self._exec_id) + assert isinstance(info, dict) + return info @property - def stream(self) -> Union[Iterator[bytes], Iterator[str]]: + def stream(self) -> Iterator[bytes] | Iterator[str]: def decoded_stream() -> Iterator[str]: assert self.encoding for line_bytes in self._stream: @@ -81,13 +90,13 @@ def decoded_stream() -> Iterator[str]: yield from decoded_stream() @property - def host_pid(self) -> Optional[int]: + def host_pid(self) -> int | None: if not self._pid_host: - self._pid_host = self._inspect()['Pid'] + self._pid_host = self._inspect()["Pid"] return self._pid_host @property - def pid(self) -> Optional[int]: + def pid(self) -> int | None: host_pid = self.host_pid if not self._pid and host_pid: self._pid = host_pid_to_container_pid(host_pid) @@ -98,9 +107,9 @@ def finished(self) -> bool: return self.returncode is not None @property - def returncode(self) -> Optional[int]: + def returncode(self) -> int | None: if self._returncode is None: - self._returncode = self._inspect()['ExitCode'] + self._returncode = self._inspect()["ExitCode"] return self._returncode def send_signal(self, sig: int) -> None: @@ -114,10 +123,10 @@ def send_signal(self, sig: int) -> None: pid = self.pid docker_container = self._container._docker logger.debug(f"sending signal {sig} to process {pid}") - cmd = f'kill -{sig} -{pid}' + cmd = f"kill -{sig} -{pid}" if pid: docker_container.exec_run(cmd, - stdout=False, stderr=False, user='root') + stdout=False, stderr=False, user="root") def kill(self) -> None: """Kills the process via a SIGKILL signal.""" @@ -127,11 +136,11 @@ def terminate(self) -> None: """Terminates the process via a SIGTERM signal.""" self.send_signal(signal.SIGTERM) - def poll(self) -> Optional[int]: + def poll(self) -> int | None: """Checks if the process has terminated and returns its returncode.""" return self.returncode - def wait(self, time_limit: Optional[float] = None) -> int: + def wait(self, time_limit: float | None = None) -> int: """Blocks until the process has terminated. Parameters diff --git a/src/dockerblade/shell.py b/src/dockerblade/shell.py index e7cd793..0ec338a 100644 --- a/src/dockerblade/shell.py +++ b/src/dockerblade/shell.py @@ -1,15 +1,14 @@ -# -*- coding: utf-8 -*- -__all__ = ('Shell', 'CompletedProcess', 'CalledProcessError') +__all__ = ("Shell", "CompletedProcess", "CalledProcessError") import typing as t -from typing_extensions import Literal +from pathlib import Path +from typing import Literal -from loguru import logger import attr import psutil +from loguru import logger -from .exceptions import (CalledProcessError, EnvNotFoundError, - ContainerFileNotFound) +from .exceptions import CalledProcessError, ContainerFileNotFound, EnvNotFoundError from .popen import Popen from .stopwatch import Stopwatch from .util import quote_container @@ -36,7 +35,7 @@ class CompletedProcess: args: str returncode: int duration: float - output: t.Optional[t.Union[str, bytes]] + output: str | bytes | None def check_returncode(self) -> None: """Raises a CalledProcessError if returncode is non-zero. @@ -56,6 +55,7 @@ def check_returncode(self) -> None: @attr.s(eq=False, hash=False, slots=True) class Shell: """Provides shell access to a Docker container. + Do not directly call the constructor to create shells. Instead, use the :method:`shell` method of :class:`Container` to build a shell. @@ -77,14 +77,14 @@ class Shell: ContainerFileNotFound If a given source file is not found. """ - container: 'Container' = attr.ib() + container: "Container" = attr.ib() path: str = attr.ib() _sources: t.Sequence[str] = attr.ib(factory=tuple) _environment: t.Mapping[str, str] = attr.ib(factory=dict) def __attrs_post_init__(self) -> None: - object.__setattr__(self, '_sources', tuple(self._sources)) - object.__setattr__(self, '_environment', dict(self._environment)) + object.__setattr__(self, "_sources", tuple(self._sources)) + object.__setattr__(self, "_environment", dict(self._environment)) if self._sources: filesystem = self.container.filesystem() @@ -94,24 +94,24 @@ def __attrs_post_init__(self) -> None: path=src) command = ' && '.join([f'. {src} > /dev/null 2> /dev/null' for src in self._sources]) # noqa - command += ' && env' + command += " && env" else: - command = 'env' + command = "env" # store the state of the environment - env: t.Dict[str, str] = {} + env: dict[str, str] = {} env_output = self.check_output(command, text=True) - for env_line in env_output.replace('\r', '').split('\n'): - name, separator, value = env_line.partition('=') + for env_line in env_output.replace("\r", "").split("\n"): + name, separator, value = env_line.partition("=") env[name] = value # ignore the PWD variable - if 'PWD' in env: - del env['PWD'] + if "PWD" in env: + del env["PWD"] - object.__setattr__(self, '_environment', env) + object.__setattr__(self, "_environment", env) - def _local_to_host_pid(self, pid_local: int) -> t.Optional[int]: + def _local_to_host_pid(self, pid_local: int) -> int | None: """Finds the host PID for a process inside this shell. Parameters @@ -128,10 +128,10 @@ def _local_to_host_pid(self, pid_local: int) -> t.Optional[int]: ctr_pids = [container.pid] info = container._info ctr_pids += \ - [container._exec_id_to_host_pid(i) for i in info['ExecIDs']] + [container._exec_id_to_host_pid(i) for i in info["ExecIDs"]] # obtain a list of all processes inside this container - ctr_procs: t.List[psutil.Process] = [] + ctr_procs: list[psutil.Process] = [] for pid in ctr_pids: proc = psutil.Process(pid) ctr_procs.append(proc) @@ -139,13 +139,15 @@ def _local_to_host_pid(self, pid_local: int) -> t.Optional[int]: # read /proc/PID/status to find the namespace mapping for proc in ctr_procs: - fn_proc = f'/proc/{proc.pid}/status' - with open(fn_proc, 'r') as fh_proc: - lines = filter(lambda l: l.startswith('NSpid'), - fh_proc.readlines()) + fn_proc = Path(f"/proc/{proc.pid}/status") + with fn_proc.open() as fh_proc: + lines = filter( + lambda line: line.startswith("NSpid"), + fh_proc.readlines(), + ) for line in lines: proc_host_pid, proc_local_pid = \ - [int(p) for p in line.strip().split('\t')[1:3]] + (int(p) for p in line.strip().split("\t")[1:3]) if proc_local_pid == pid_local: return proc_host_pid @@ -154,22 +156,22 @@ def _local_to_host_pid(self, pid_local: int) -> t.Optional[int]: def _instrument(self, command: str, *, - time_limit: t.Optional[int] = None, - kill_after: int = 1 + time_limit: int | None = None, + kill_after: int = 1, ) -> str: q = quote_container logger.debug(f"instrumenting command: {command}") - command = f'{self.path} -c {q(command)}' + command = f"{self.path} -c {q(command)}" if time_limit: - command = (f'timeout --kill-after={kill_after} ' - f'--signal=SIGTERM {time_limit} {command}') + command = (f"timeout --kill-after={kill_after} " + f"--signal=SIGTERM {time_limit} {command}") logger.debug(f"instrumented command: {command}") return command def send_signal(self, pid: int, sig: int) -> None: # FIXME run as root! logger.debug(f"sending signal {sig} to process {pid}") - cmd = f'kill -{sig} {pid}' + cmd = f"kill -{sig} {pid}" self.run(cmd) def environ(self, var: str) -> str: @@ -189,13 +191,12 @@ def check_call( self, args: str, *, - cwd: str = '/', - time_limit: t.Optional[int] = None, - kill_after: int = 1, - environment: t.Optional[t.Mapping[str, str]] = None, + cwd: str = "/", + time_limit: int | None = None, # noqa: ARG002 + kill_after: int = 1, # noqa: ARG002 + environment: t.Mapping[str, str] | None = None, ) -> None: - """Executes a given commands, blocks until its completion, and checks - that the return code is zero. + """Executes a given commands, blocks until completion, and checks return code is zero. Raises ------ @@ -215,12 +216,12 @@ def check_output( args: str, *, stderr: bool = True, - cwd: str = '/', - encoding: str = 'utf-8', + cwd: str = "/", + encoding: str = "utf-8", text: Literal[False], - time_limit: t.Optional[int] = None, + time_limit: int | None = None, kill_after: int = 1, - environment: t.Optional[t.Mapping[str, str]] = None, + environment: t.Mapping[str, str] | None = None, ) -> bytes: ... @@ -230,12 +231,12 @@ def check_output( args: str, *, stderr: bool = True, - cwd: str = '/', - encoding: str = 'utf-8', + cwd: str = "/", + encoding: str = "utf-8", text: Literal[True], - time_limit: t.Optional[int] = None, + time_limit: int | None = None, kill_after: int = 1, - environment: t.Optional[t.Mapping[str, str]] = None, + environment: t.Mapping[str, str] | None = None, ) -> str: ... @@ -244,15 +245,14 @@ def check_output( args: str, *, stderr: bool = False, - cwd: str = '/', - encoding: str = 'utf-8', + cwd: str = "/", + encoding: str = "utf-8", text: bool = True, - time_limit: t.Optional[int] = None, + time_limit: int | None = None, kill_after: int = 1, - environment: t.Optional[t.Mapping[str, str]] = None, - ) -> t.Union[str, bytes]: - """Executes a given commands, blocks until its completion, and checks - that the return code is zero. + environment: t.Mapping[str, str] | None = None, + ) -> str | bytes: + """Executes a given command, blocks until completion, and checks return code is zero. Returns ------- @@ -283,14 +283,14 @@ def run( self, args: str, *, - encoding: str = 'utf-8', - cwd: str = '/', + encoding: str = "utf-8", + cwd: str = "/", text: bool = True, stdout: bool = True, stderr: bool = False, - time_limit: t.Optional[int] = None, + time_limit: int | None = None, kill_after: int = 1, - environment: t.Optional[t.Mapping[str, str]] = None, + environment: t.Mapping[str, str] | None = None, ) -> CompletedProcess: """Executes a given command and blocks until its completion. @@ -352,11 +352,11 @@ def run( logger.debug(f"retcode: {retcode}") - output: t.Optional[t.Union[str, bytes]] + output: str | bytes | None if no_output: output = None elif text: - output = output_bin.decode(encoding).rstrip('\r\n') + output = output_bin.decode(encoding).rstrip("\r\n") else: output = output_bin @@ -371,13 +371,13 @@ def popen( self, args: str, *, - cwd: str = '/', - encoding: t.Optional[str] = 'utf-8', + cwd: str = "/", + encoding: str | None = "utf-8", stdout: bool = True, stderr: bool = False, - time_limit: t.Optional[int] = None, + time_limit: int | None = None, kill_after: int = 1, - environment: t.Optional[t.Mapping[str, str]] = None, + environment: t.Mapping[str, str] | None = None, ) -> Popen: docker_api = self.container.daemon.api if not environment: @@ -393,14 +393,16 @@ def popen( workdir=cwd, stdout=stdout, stderr=stderr) - exec_id = exec_response['Id'] + exec_id = exec_response["Id"] exec_stream = docker_api.exec_start(exec_id, stream=True) - logger.debug(f'started Exec [{exec_id}] for Popen') - return Popen(args=args, - cwd=cwd, - container=self.container, - docker_api=docker_api, - exec_id=exec_id, - encoding=encoding, - stream=exec_stream) + logger.debug(f"started Exec [{exec_id}] for Popen") + return Popen( + args=args, + cwd=cwd, + container=self.container, + docker_api=docker_api, + exec_id=exec_id, + encoding=encoding, + stream=exec_stream, + ) diff --git a/src/dockerblade/stopwatch.py b/src/dockerblade/stopwatch.py index dfc4ae5..0c430ed 100644 --- a/src/dockerblade/stopwatch.py +++ b/src/dockerblade/stopwatch.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -__all__ = ('Stopwatch',) +__all__ = ("Stopwatch",) +import warnings from timeit import default_timer as timer from types import TracebackType -from typing import Optional, Type -import warnings import attr @@ -24,23 +22,23 @@ class Stopwatch: _paused: bool = attr.ib(default=True) _time_start: float = attr.ib(default=0.0) - def __enter__(self) -> 'Stopwatch': + def __enter__(self) -> "Stopwatch": self.start() return self def __exit__(self, - ex_type: Optional[Type[BaseException]], - ex_val: Optional[BaseException], - ex_tb: Optional[TracebackType] + ex_type: type[BaseException] | None, + ex_val: BaseException | None, + ex_tb: TracebackType | None, ) -> None: self.stop() def __repr__(self) -> str: - return f'Stopwatch(paused={self._paused}, duration={self.duration})' + return f"Stopwatch(paused={self._paused}, duration={self.duration})" def __str__(self) -> str: status = "PAUSED" if self._paused else "RUNNING" - return f'[{status}] {self.duration:.3f} s' + return f"[{status}] {self.duration:.3f} s" def stop(self) -> None: """Freezes the stopwatch.""" @@ -54,7 +52,10 @@ def start(self) -> None: self._time_start = timer() self._paused = False else: - warnings.warn("timer is already running") + warnings.warn( + "timer is already running", + stacklevel=2, + ) @property def paused(self) -> bool: diff --git a/src/dockerblade/util.py b/src/dockerblade/util.py index ecc6335..10482d8 100644 --- a/src/dockerblade/util.py +++ b/src/dockerblade/util.py @@ -1,18 +1,17 @@ -# -*- coding: utf-8 -*- """Provides a number of utility methods.""" -__all__ = ('quote_host', 'quote_container') +__all__ = ("quote_host", "quote_container") import functools import os import shlex -from typing import Callable +from collections.abc import Callable import mslex quote_host: Callable[[str], str] quote_container = shlex.quote -if os.name == 'nt': +if os.name == "nt": quote_host = functools.partial(mslex.quote, for_cmd=True) else: quote_host = shlex.quote diff --git a/src/dockerblade/version.py b/src/dockerblade/version.py index a80cec3..906d362 100644 --- a/src/dockerblade/version.py +++ b/src/dockerblade/version.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- -__version__ = '0.5.8' +__version__ = "0.6.0"