├── .circleci └── config.yml ├── .github ├── CODEOWNERS └── dco.yml ├── .gitignore ├── .idea ├── .gitignore ├── git_toolbox_prj.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── markdown.xml ├── misc.xml ├── modules.xml ├── python-ansible-vault-rotate.iml ├── vcs.xml └── webResources.xml ├── .pre-commit-config.yaml ├── .releaserc.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ansible_vault_rotate ├── __init__.py ├── cli │ ├── __init__.py │ ├── __testdata__ │ │ ├── glob │ │ │ ├── file1 │ │ │ ├── file2 │ │ │ └── nested │ │ │ │ └── file1 │ │ └── vaulted │ │ │ └── file.yml │ ├── cli_args.py │ ├── cli_args_test.py │ ├── config.py │ ├── config_test.py │ ├── glob.py │ ├── glob_test.py │ ├── logging.py │ ├── logging_test.py │ ├── rotator.py │ ├── rotator_test.py │ ├── run.py │ ├── tui.py │ └── tui_test.py ├── match │ ├── __init__.py │ ├── __testdata__ │ │ ├── label_vaulted.yml │ │ ├── nested_vaulted.yml │ │ ├── single_vaulted.yml │ │ └── single_vaulted_eof.yml │ ├── find.py │ └── test_find.py └── vault │ ├── __init__.py │ ├── __testdata__ │ ├── TEST │ ├── multiple-secret.yml │ ├── multiple-vault.yml │ ├── single-secret-eof.yml │ ├── single-secret.yml │ ├── vault-password │ └── vaulted-file.txt │ ├── detect.py │ ├── detect_test.py │ ├── file.py │ ├── file_test.py │ ├── string.py │ ├── string_test.py │ ├── util.py │ ├── vault_source.py │ └── vault_source_test.py ├── codecov.yml ├── poetry.lock ├── pyproject.toml └── renovate.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | python: circleci/python@3.0.0 5 | codecov: codecov/codecov@5.0.3 6 | semantic-release: trustedshops-public/semantic-release@6.0.0 7 | 8 | jobs: 9 | pip-publish: 10 | executor: python/default 11 | steps: 12 | - checkout 13 | - python/install-packages: 14 | pkg-manager: poetry 15 | - run: 16 | name: Publish package 17 | command: | 18 | if [ -z "$CIRCLE_TAG" ] 19 | then 20 | echo "Building for snapshot, replacing version with unique one" 21 | last_tag=$(git describe --tags `git rev-list --tags --max-count=1`) 22 | version="${last_tag}.dev${CIRCLE_BUILD_NUM}" 23 | sed -ri "s/version = \"(.*)\"/version = \"$version\"/" pyproject.toml 24 | 25 | poetry publish \ 26 | --build \ 27 | --repository testpypi \ 28 | --username "$TWINE_USERNAME" \ 29 | --password "$TWINE_PASSWORD" 30 | else 31 | poetry publish \ 32 | --build \ 33 | --username "$TWINE_USERNAME" \ 34 | --password "$TWINE_PASSWORD" 35 | fi 36 | 37 | test: 38 | executor: python/default 39 | steps: 40 | - checkout 41 | - python/install-packages: 42 | pkg-manager: poetry 43 | - run: 44 | name: Run tests 45 | command: | 46 | poetry run coverage run -m pytest --junit-xml test-results/junit.xml 47 | poetry run coverage report 48 | poetry run coverage html 49 | poetry run coverage xml -i 50 | - store_artifacts: 51 | path: htmlcov 52 | - store_test_results: 53 | path: test-results 54 | - codecov/upload 55 | 56 | workflows: 57 | continuous: 58 | jobs: 59 | - test 60 | - pip-publish: 61 | name: publish-testpypi 62 | requires: 63 | - test 64 | filters: 65 | branches: 66 | only: main 67 | context: 68 | - pip-test 69 | 70 | - pip-publish: 71 | name: publish-pypi 72 | filters: 73 | branches: 74 | ignore: /.*/ 75 | tags: 76 | only: /.*/ 77 | context: 78 | - pip-live 79 | - semantic-release/with_existing_config: 80 | name: semantic-release 81 | additional_packages: "@google/semantic-release-replace-plugin" 82 | requires: 83 | - test 84 | context: 85 | - semantic-release 86 | filters: 87 | branches: 88 | only: 89 | - main 90 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @trustedshops-public/ansible 2 | -------------------------------------------------------------------------------- /.github/dco.yml: -------------------------------------------------------------------------------- 1 | require: 2 | members: false 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | aws.xml 10 | discord.xml 11 | -------------------------------------------------------------------------------- /.idea/git_toolbox_prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/markdown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/python-ansible-vault-rotate.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: check-json 7 | - id: check-merge-conflict 8 | - id: check-yaml 9 | exclude: (.circleci/config.yml|.*__testdata__.*) 10 | - id: detect-private-key 11 | - id: check-symlinks 12 | - id: check-vcs-permalinks 13 | - id: trailing-whitespace 14 | args: 15 | - --markdown-linebreak-ext=md 16 | - id: mixed-line-ending 17 | args: 18 | - --fix=lf 19 | - id: check-case-conflict 20 | - id: check-executables-have-shebangs 21 | - id: check-toml 22 | - id: check-xml 23 | - id: fix-byte-order-marker 24 | - id: destroyed-symlinks 25 | 26 | - repo: https://github.com/syntaqx/git-hooks 27 | rev: v0.0.18 28 | hooks: 29 | - id: circleci-config-validate 30 | - id: shellcheck 31 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master", 4 | "main" 5 | ], 6 | "plugins": [ 7 | [ 8 | "@semantic-release/commit-analyzer", 9 | { 10 | "preset": "conventionalcommits" 11 | } 12 | ], 13 | [ 14 | "@semantic-release/release-notes-generator", 15 | { 16 | "preset": "conventionalcommits" 17 | } 18 | ], 19 | [ 20 | "@semantic-release/changelog", 21 | { 22 | "changelogFile": "CHANGELOG.md" 23 | } 24 | ], 25 | [ 26 | "@google/semantic-release-replace-plugin", 27 | { 28 | "replacements": [ 29 | { 30 | "files": [ 31 | "pyproject.toml" 32 | ], 33 | "from": "version = \"[0-9.]+\"", 34 | "to": "version = \"${nextRelease.version}\"", 35 | "results": [ 36 | { 37 | "file": "pyproject.toml", 38 | "hasChanged": true 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | ], 45 | [ 46 | "@semantic-release/git", 47 | { 48 | "assets": [ 49 | "CHANGELOG.md", 50 | "pyproject.toml" 51 | ] 52 | } 53 | ], 54 | [ 55 | "@semantic-release/github", 56 | { 57 | "path": "semantic-release", 58 | "name": "trustedshops-public/python-ansible-vault-rotate" 59 | } 60 | ] 61 | ], 62 | "tagFormat": "${version}" 63 | } 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.1.0](https://github.com/trustedshops-public/python-ansible-vault-rotate/compare/2.0.0...2.1.0) (2024-12-19) 2 | 3 | 4 | ### Features 5 | 6 | * allow rekey when multiple vaults are used within one file ([db26807](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/db2680790e247e97b7f55100513fe1611697039f)) 7 | 8 | ## [2.0.0](https://github.com/trustedshops-public/python-ansible-vault-rotate/compare/1.1.2...2.0.0) (2023-05-02) 9 | 10 | 11 | ### ⚠ BREAKING CHANGES 12 | 13 | * Add interactive TUI (#2) 14 | 15 | ### Features 16 | 17 | * Add interactive TUI ([#2](https://github.com/trustedshops-public/python-ansible-vault-rotate/issues/2)) ([07466b3](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/07466b3dd2a5317fdfa8e05a3613e84deb44b5c7)) 18 | 19 | ## [1.1.2](https://github.com/trustedshops-public/python-ansible-vault-rotate/compare/1.1.1...1.1.2) (2023-03-23) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * rekey of encrypted values at the end of the file ([277fb02](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/277fb025f508aeb6aacd2fc6d1f96b3c89b799ff)) 25 | 26 | ## [1.1.1](https://github.com/trustedshops-public/python-ansible-vault-rotate/compare/1.1.0...1.1.1) (2023-01-13) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * Fix test ([95ac651](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/95ac6510d7cbe54a3202bf8c5b2b22c4041f1ba2)) 32 | 33 | ## [1.1.0](https://github.com/trustedshops-public/python-ansible-vault-rotate/compare/1.0.0...1.1.0) (2023-01-13) 34 | 35 | 36 | ### Features 37 | 38 | * Add better error handling for loading vault sources ([c15c1f3](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/c15c1f30b0324173bc339139bd8882d0c93f26c5)) 39 | 40 | ## 1.0.0 (2023-01-11) 41 | 42 | 43 | ### Features 44 | 45 | * Add file recrypt ([8deaa21](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/8deaa218679d2a94d7f467fc57e9fca3383d716f)) 46 | * Add first working version of CLI ([641e781](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/641e7818c41181b9ed52d8277f537abfd5908a3c)) 47 | * Add first working version of CLI ([f466351](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/f466351803152f6cb95fb1ef573610e8caa26bf2)) 48 | * Add match and vault functionality ([9667e78](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/9667e78cb6852a9330845c81b88d6743dbe8012b)) 49 | * Add update support for vault file ([b3012c9](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/b3012c9c40d442e3364f5584c5772284d416be3e)) 50 | * First release ([6eeedd2](https://github.com/trustedshops-public/python-ansible-vault-rotate/commit/6eeedd273978272f8c6910d39789f410601e8912)) 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to our Ansible Vault Rotate CLI 2 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the configuration 6 | - Submitting a fix 7 | - Proposing new features 8 | - Becoming a maintainer 9 | 10 | ## We Develop with GitHub 11 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 12 | 13 | ## We Use [CircleCI](https://circleci.com/product/), So All Code Changes Happen Through Pull Requests 14 | Pull requests are the best way to propose changes to the codebase (we use [CircleCI](https://circleci.com/product/)). We actively welcome your pull requests: 15 | 16 | 1. Fork the repo and create your branch from `main`. 17 | 2. If you've added code that should be tested, add tests. 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Ensure the tests pass. 20 | 5. Make sure your code and commit lints. 21 | 6. Issue that pull request! 22 | 23 | ## Any contributions you make will be under the MIT License 24 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](https://opensource.org/licenses/MIT) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using GitHub's issues 27 | We use GitHub issues to track public bugs. Report a bug by opening a new issue, it's that easy! 28 | 29 | ## Write bug reports with detail, background, and sample code 30 | 31 | **Great Bug Reports** tend to have: 32 | 33 | - A quick summary and/or background 34 | - Steps to reproduce 35 | - Be specific! 36 | - Give sample code if you can. 37 | - What you expected would happen 38 | - What actually happens 39 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 40 | 41 | People *love* thorough bug reports. I'm not even kidding. 42 | 43 | To make your life easier there is also a handy template available so feel free to use it. 44 | 45 | ## License 46 | By contributing, you agree that your contributions will be licensed under its MIT License. 47 | 48 | ## Developer Certificate of Origin 49 | Every external contributor needs to sign commits with a valid DCO. 50 | 51 | This is done by adding a Signed-off-by line to commit messages. 52 | 53 | ``` 54 | This is my commit message 55 | 56 | Signed-off-by: Random J Developer 57 | ``` 58 | 59 | Git even has a -s command line option to append this automatically to your commit message: 60 | 61 | ``` 62 | git commit -s -m 'This is my commit message' 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TrustedShops 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-ansible-vault-rotate 2 | === 3 | [![GitHub License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/trustedshops-public/spring-boot-starter-keycloak-path-based-resolver/blob/main/LICENSE) 4 | [![pre-commit](https://img.shields.io/badge/%E2%9A%93%20%20pre--commit-enabled-success)](https://pre-commit.com/) 5 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/trustedshops-public/python-ansible-vault-rotate/tree/main.svg?style=shield&circle-token=9c1ea1cc46c804b46f457772637c8481717b511a)](https://dl.circleci.com/status-badge/redirect/gh/trustedshops-public/python-ansible-vault-rotate/tree/main) 6 | [![PyPI version](https://badge.fury.io/py/ansible-vault-rotate.svg)](https://pypi.org/project/ansible-vault-rotate) 7 | [![codecov](https://codecov.io/gh/trustedshops-public/python-ansible-vault-rotate/branch/main/graph/badge.svg?token=6PJ1GJzIcB)](https://codecov.io/gh/trustedshops-public/python-ansible-vault-rotate) 8 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=trustedshops-public_python-ansible-vault-rotate&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=trustedshops-public_python-ansible-vault-rotate) 9 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=trustedshops-public_python-ansible-vault-rotate&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=trustedshops-public_python-ansible-vault-rotate) 10 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=trustedshops-public_python-ansible-vault-rotate&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=trustedshops-public_python-ansible-vault-rotate) 11 | 12 | Advanced Python CLI to rotate the secret used for ansible vault inline secrets and files in a project 13 | 14 | ## Features 15 | 16 | - Reencrypt vault files 17 | - Reencrypt inline vaulted secrets 18 | 19 | ## Installation 20 | 21 | It is strongly recommended to use pipx instead of pip if possible: 22 | 23 | ```sh 24 | pipx install ansible-vault-rotate 25 | ``` 26 | 27 | Otherwise you can also use plain pip, but be warned that this might 28 | collide with your ansible installation globally! 29 | 30 | ```sh 31 | pip install ansible-vault-rotate 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Rekey given vault secret with new secret specified on CLI 37 | 38 | ```sh 39 | ansible-vault-rotate --old-vault-secret-source file://my-vault-password \ 40 | --new-vault-secret-source my-new-secret \ 41 | --update-source-secret 42 | ``` 43 | 44 | ## Rekey only specific files (e.g. when using multiple keys per stage) 45 | 46 | ```sh 47 | ansible-vault-rotate --old-vault-secret-source file://my-vault-password- \ 48 | --new-vault-secret-source my-new-secret \ 49 | --file-glob-pattern group_vars//*.yml \ 50 | --update-source-secret 51 | ``` 52 | 53 | ## Getting help about all args 54 | 55 | ```sh 56 | ansible-vault-rotate --help 57 | ``` 58 | 59 | ## Development 60 | 61 | For development, you will need: 62 | 63 | - Python 3.9 or greater 64 | - Poetry 65 | 66 | ### Install 67 | 68 | ``` 69 | poetry install 70 | ``` 71 | 72 | ### Run tests 73 | 74 | ``` 75 | poetry run pytest 76 | ``` 77 | -------------------------------------------------------------------------------- /ansible_vault_rotate/__init__.py: -------------------------------------------------------------------------------- 1 | __VERSION__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/__init__.py: -------------------------------------------------------------------------------- 1 | from .run import run 2 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/__testdata__/glob/file1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustedshops-public/python-ansible-vault-rotate/748121faead575e43dc0a718543ed4aaded91765/ansible_vault_rotate/cli/__testdata__/glob/file1 -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/__testdata__/glob/file2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustedshops-public/python-ansible-vault-rotate/748121faead575e43dc0a718543ed4aaded91765/ansible_vault_rotate/cli/__testdata__/glob/file2 -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/__testdata__/glob/nested/file1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustedshops-public/python-ansible-vault-rotate/748121faead575e43dc0a718543ed4aaded91765/ansible_vault_rotate/cli/__testdata__/glob/nested/file1 -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/__testdata__/vaulted/file.yml: -------------------------------------------------------------------------------- 1 | test: !vault | 2 | $ANSIBLE_VAULT;1.1;AES256 3 | 31663031633532633662666465396235383461646561373762303432373866313466376637303764 4 | 3734363362623037613935326332623039636434373562300a336639313131326135323833346634 5 | 39343737336437333161656434613064376633366435383663643836316135393336393932386530 6 | 3965643937663235320a353463623430373333373337636339313238633931343932313166363663 7 | 6161 8 | regular_key: goes here 9 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/cli_args.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser, Namespace 2 | from os import getcwd 3 | import sys 4 | 5 | from ansible_vault_rotate import __VERSION__ 6 | 7 | parser = ArgumentParser() 8 | parser.add_argument('--version', 9 | action='version', 10 | version=f'%(prog)s {__VERSION__}', 11 | help="Print current version and exit") 12 | parser.add_argument("--old-vault-secret-source", 13 | type=str, 14 | help="Source for the old secret. Valid are only plain text or file urls starting with file:// and pointing to a file relative to the current directory or absolute paths", 15 | required=True) 16 | parser.add_argument("--new-vault-secret-source", 17 | type=str, 18 | help="Source for the new secret. Valid are only plain text or file urls starting with file:// and pointing to a file relative to the current directory or absolute paths", 19 | required=True) 20 | parser.add_argument("--file-glob-pattern", 21 | type=str, 22 | action="append", 23 | help="Glob pattern to apply the rekey for.", 24 | default=[]) 25 | parser.add_argument("--ignore-errors", 26 | action="store_true", 27 | help="Do not abort on processing individual files", 28 | default=False) 29 | parser.add_argument("--pwd", 30 | type=str, 31 | help="Change working directory for execution, this also changes the relative path for file urls", 32 | default=getcwd()) 33 | parser.add_argument("--update-source-secret", 34 | action="store_true", 35 | help="Should the source secret be updated (not supported for text source)", 36 | default=False) 37 | 38 | 39 | def has_cli_args(): 40 | return len(sys.argv) != 1 41 | 42 | 43 | def parse_args() -> Namespace: 44 | return parser.parse_args() 45 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/cli_args_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | from .cli_args import has_cli_args, parse_args 4 | 5 | 6 | class CliArgsTest(unittest.TestCase): 7 | def test_has_cli_args(self): 8 | with patch("sys.argv", ["bin", "-h"]): 9 | self.assertTrue(has_cli_args()) 10 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/config.py: -------------------------------------------------------------------------------- 1 | from os import chdir 2 | 3 | from ansible_vault_rotate.vault import build_vault_source 4 | 5 | 6 | class CliConfig: 7 | def __init__(self, args: dict[str]): 8 | self.args = args 9 | 10 | self.source_vault_passphrase = None 11 | self.source_vault = None 12 | self.target_vault = None 13 | self.target_vault_passphrase = None 14 | 15 | self.file_glob_patterns = self.__parse_glob_patterns() 16 | self.ignore_errors = self.__with_default('ignore_errors', False) 17 | self.update_source_secret = self.__with_default('update_source_secret', False) 18 | 19 | def __with_default(self, name: str, default: any) -> any: 20 | if self.__has_arg(name): 21 | return self.args[name] 22 | 23 | return default 24 | 25 | def __parse_glob_patterns(self) -> list[str]: 26 | patterns = self.__with_default('file_glob_pattern', []) 27 | if len(patterns) == 0: 28 | patterns = ["**/*.yml", "**/*.yaml"] 29 | 30 | return patterns 31 | 32 | def __has_arg(self, name: str) -> bool: 33 | return name in self.args and self.args[name] is not None 34 | 35 | def switch_to_pwd(self) -> "CliConfig": 36 | if self.has_custom_pwd(): 37 | chdir(self.args['pwd']) 38 | 39 | return self 40 | 41 | def has_custom_pwd(self): 42 | return self.__has_arg("pwd") 43 | 44 | def to_cli_call_string(self): 45 | return "ansible-vault-rotate " + " ".join([ 46 | " ".join(map(lambda pattern : f"--file-glob-pattern '{pattern}'",self.file_glob_patterns)), 47 | f"--old-vault-secret-source '{self.args['old_vault_secret_source']}'", 48 | f"--new-vault-secret-source '{self.args['new_vault_secret_source']}'", 49 | "--ignore-errors" if self.ignore_errors else "", 50 | "--update-source-secret" if self.update_source_secret else "", 51 | f"--pwd '{self.args['pwd']}'" if self.has_custom_pwd() else "" 52 | ]).strip() 53 | 54 | def parse_vaults(self) -> "CliConfig": 55 | self.source_vault = build_vault_source(self.args['old_vault_secret_source']) 56 | self.source_vault_passphrase = self.source_vault.read() 57 | 58 | self.target_vault = build_vault_source(self.args['new_vault_secret_source']) 59 | self.target_vault_passphrase = self.target_vault.read() 60 | 61 | return self 62 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/config_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .config import CliConfig 3 | 4 | 5 | class ConfigTest(unittest.TestCase): 6 | def test_glob_pattern_default(self): 7 | config = CliConfig({}) 8 | self.assertEqual(config.file_glob_patterns, ["**/*.yml", "**/*.yaml"]) 9 | 10 | def test_glob_pattern_custom(self): 11 | config = CliConfig({ 12 | 'file_glob_pattern': ['**/*.yml'] 13 | }) 14 | self.assertEqual(len(config.file_glob_patterns), 1) 15 | self.assertEqual(config.file_glob_patterns[0], "**/*.yml") 16 | 17 | def test_parse(self): 18 | config = CliConfig({ 19 | 'old_vault_secret_source': "old-secret", 20 | 'new_vault_secret_source': "new-secret", 21 | }) 22 | config.parse_vaults() 23 | 24 | self.assertIsNotNone(config.source_vault) 25 | self.assertIsNotNone(config.target_vault) 26 | 27 | self.assertEqual(config.source_vault_passphrase, "old-secret") 28 | self.assertEqual(config.target_vault_passphrase, "new-secret") 29 | 30 | self.assertFalse(config.has_custom_pwd()) 31 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/glob.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from glob import glob 3 | from os.path import isfile 4 | 5 | 6 | def iterate_patterns(file_glob_pattern: list[str]) -> typing.Generator[str, None, None]: 7 | """ 8 | Iterate all files in the given glob patterns, ignoring folders 9 | :param file_glob_pattern: Patterns to apply to glob 10 | :return: Iterator emitting file names 11 | """ 12 | for pattern in file_glob_pattern: 13 | for file in glob(pattern, recursive=True): 14 | if isfile(file): 15 | yield file 16 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/glob_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from .glob import iterate_patterns 4 | 5 | 6 | class GlobTest(unittest.TestCase): 7 | def __build_glob(self, relative_pattern: str) -> str: 8 | return os.path.dirname(os.path.abspath(__file__)) + f"/__testdata__/glob/{relative_pattern}" 9 | 10 | def test_top_level_glob(self): 11 | files = [file for file in iterate_patterns([self.__build_glob("*")])] 12 | self.assertEqual(len(files), 2) 13 | 14 | def test_nested_glob(self): 15 | files = [file for file in iterate_patterns([self.__build_glob("**/*")])] 16 | self.assertEqual(len(files), 3) 17 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class FormattingConsoleLogHandler(logging.StreamHandler): 5 | colors = { 6 | logging.DEBUG: '\033[37m', 7 | logging.INFO: '\033[34m', 8 | logging.WARNING: '\033[33m', 9 | logging.ERROR: '\033[31m', 10 | logging.CRITICAL: '\033[101m', 11 | } 12 | reset = '\033[0m' 13 | my_formatter = logging.Formatter('\033[37m[%(filename)10s:%(lineno)s]\033[0m \033[37m---\033[0m %(message)s') 14 | 15 | def format(self, record): 16 | color = self.colors[record.levelno] 17 | log = self.my_formatter.format(record) 18 | reset = self.reset 19 | return color + "%7s " % record.levelname + reset + log 20 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/logging_test.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | from .logging import FormattingConsoleLogHandler 4 | from logging import LogRecord 5 | 6 | 7 | class LoggingTest(unittest.TestCase): 8 | def test_format(self): 9 | handler = FormattingConsoleLogHandler() 10 | record = LogRecord("test", logging.INFO, "test.py", 20, "my message", {}, None) 11 | formatted = handler.format(record) 12 | self.assertIsNotNone(formatted) 13 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/rotator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .config import CliConfig 3 | from .glob import iterate_patterns 4 | from ..vault import has_vault_secrets, rekey_file 5 | 6 | 7 | class AnsibleVaultRotator: 8 | def __init__(self, config: CliConfig): 9 | self.config = config 10 | self.error_count = 0 11 | self.rekeyed_files_count = 0 12 | self.rekey_secrets_count = 0 13 | 14 | def iterate_files(self) -> "AnsibleVaultRotator": 15 | for file_path in iterate_patterns(self.config.file_glob_patterns): 16 | logging.debug("Found file %s", file_path) 17 | if not has_vault_secrets(file_path): 18 | logging.debug("File has no secrets, skipping") 19 | continue 20 | 21 | logging.info("Processing file %s", file_path) 22 | try: 23 | self.__track_secrets_count( 24 | rekey_file(file_path, 25 | self.config.source_vault_passphrase, 26 | self.config.target_vault_passphrase) 27 | ) 28 | except Exception as e: 29 | self.__handle_error(file_path, e) 30 | 31 | return self 32 | 33 | def __handle_error(self, file_path: str, e: Exception) -> None: 34 | self.error_count += 1 35 | if self.config.ignore_errors: 36 | logging.warning("Failed to rekey file %s: %s", file_path, e) 37 | else: 38 | logging.error("Failed to rekey file %s: %s", file_path, e) 39 | raise e 40 | 41 | def __track_secrets_count(self, secrets_count: int) -> None: 42 | self.rekey_secrets_count += secrets_count 43 | self.rekeyed_files_count += 1 44 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/rotator_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from .rotator import AnsibleVaultRotator 4 | from .config import CliConfig 5 | import tempfile 6 | import shutil 7 | 8 | 9 | class AnsibleVaultRotatorTest(unittest.TestCase): 10 | def data_folder(self, path): 11 | return os.path.dirname(os.path.abspath(__file__)) + f"/__testdata__/{path}" 12 | 13 | def test_no_files(self): 14 | with tempfile.TemporaryDirectory() as tmpdir: 15 | config = CliConfig({ 16 | 'old_vault_secret_source': 'test', 17 | 'new_vault_secret_source': 'test', 18 | 'pwd': str(tmpdir) 19 | }) 20 | config.switch_to_pwd() 21 | config.parse_vaults() 22 | rotator = AnsibleVaultRotator(config) 23 | rotator.iterate_files() 24 | self.assertEqual(rotator.rekeyed_files_count, 0) 25 | self.assertEqual(rotator.rekey_secrets_count, 0) 26 | self.assertEqual(rotator.error_count, 0) 27 | 28 | def test_valid_vault(self): 29 | with tempfile.TemporaryDirectory() as tmpdir: 30 | path = shutil.copytree(self.data_folder("vaulted"), str(tmpdir) + "/test") 31 | config = CliConfig({ 32 | 'old_vault_secret_source': 'test', 33 | 'new_vault_secret_source': 'test', 34 | 'pwd': str(path) 35 | }) 36 | config.switch_to_pwd() 37 | config.parse_vaults() 38 | rotator = AnsibleVaultRotator(config) 39 | rotator.iterate_files() 40 | self.assertEqual(rotator.rekeyed_files_count, 1) 41 | self.assertEqual(rotator.rekey_secrets_count, 1) 42 | self.assertEqual(rotator.error_count, 0) 43 | 44 | def test_invalid_vault(self): 45 | with tempfile.TemporaryDirectory() as tmpdir: 46 | path = shutil.copytree(self.data_folder("vaulted"), str(tmpdir) + "/test") 47 | config = CliConfig({ 48 | 'old_vault_secret_source': 'test123', 49 | 'new_vault_secret_source': 'test', 50 | 'pwd': str(path), 51 | 'ignore_errors': True, 52 | }) 53 | config.switch_to_pwd() 54 | config.parse_vaults() 55 | rotator = AnsibleVaultRotator(config) 56 | rotator.iterate_files() 57 | self.assertEqual(rotator.rekeyed_files_count, 0) 58 | self.assertEqual(rotator.rekey_secrets_count, 0) 59 | self.assertEqual(rotator.error_count, 1) 60 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | 4 | from .logging import FormattingConsoleLogHandler 5 | from .cli_args import parse_args, has_cli_args 6 | from .config import CliConfig 7 | from .rotator import AnsibleVaultRotator 8 | from .tui import prompt_tui 9 | 10 | 11 | def run() -> None: 12 | """ 13 | Entrypoint for CLI 14 | """ 15 | logging.basicConfig(level=logging.INFO, handlers=[FormattingConsoleLogHandler()]) 16 | if has_cli_args(): 17 | args = parse_args() 18 | args = args.__dict__ 19 | is_interactive = False 20 | else: 21 | args = prompt_tui() 22 | is_interactive = True 23 | print() 24 | 25 | logging.debug("Arguments provided: %s", args) 26 | 27 | config = CliConfig(args) 28 | config.switch_to_pwd() 29 | 30 | try: 31 | config.parse_vaults() 32 | except Exception as e: 33 | logging.error("Failed to load vault sources: %s", e) 34 | sys.exit(2) 35 | 36 | rotator = AnsibleVaultRotator(config) 37 | try: 38 | rotator.iterate_files() 39 | except Exception as e: 40 | sys.exit(1) 41 | 42 | logging.info(f"Finished rotation | " 43 | f"Errors: {rotator.error_count}, " 44 | f"Rekeyed secrets: {rotator.rekey_secrets_count}, " 45 | f"Rekeyed files: {rotator.rekeyed_files_count}") 46 | 47 | if config.update_source_secret: 48 | logging.info("Try to update source secret") 49 | config.source_vault.write(config.target_vault_passphrase) 50 | 51 | if is_interactive: 52 | print("") 53 | print("\033[1mThe options you chose resulted in the following CLI call. You can use that for automation as well.\033[0m") 54 | print(f" \033[3m{config.to_cli_call_string()}\033[0m") 55 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/tui.py: -------------------------------------------------------------------------------- 1 | from InquirerPy import prompt, get_style 2 | from InquirerPy.validator import PathValidator 3 | from os import getcwd 4 | from unittest.mock import patch 5 | 6 | 7 | VAULT_TYPE_FILE = "file" 8 | VAULT_TYPE_PLAIN_TEXT = "plain text" 9 | VAULT_TYPES = [ 10 | VAULT_TYPE_PLAIN_TEXT, 11 | VAULT_TYPE_FILE, 12 | ] 13 | 14 | 15 | def validate_present(value: str) -> bool: 16 | """ 17 | Validate a given parameter is present 18 | :param value: Value to check 19 | """ 20 | return len(value) > 0 21 | 22 | 23 | def when_type(specifier: str, type: str) -> callable: 24 | """ 25 | Run the given input when the vault is of the given type 26 | :param specifier: Specifier of vault (old, new) 27 | :param type: Type of the vault source secret 28 | """ 29 | def inner(result): 30 | return result[f"{specifier}_vault.type"] == type 31 | 32 | return inner 33 | 34 | 35 | def remap_vault_source(args: dict[str], specifier: str) -> None: 36 | """ 37 | Remap the vault source from interactive input 38 | :param args: Arguments collected 39 | :param specifier: Specifier of vault (old, new) 40 | """ 41 | prefix = "" 42 | vault_type = args[f"{specifier}_vault.type"] 43 | normalized_vault_type = vault_type.replace(" ","_") 44 | 45 | if vault_type == "file": 46 | prefix = "file://" 47 | 48 | args[f"{specifier}_vault_secret_source"] = f"{prefix}{args[f'{specifier}_vault.value.{normalized_vault_type}']}" 49 | 50 | 51 | validate_directory = PathValidator(is_dir=False, message="Input is not a file") 52 | questions = [ 53 | { 54 | "type": "list", 55 | "name": "old_vault.type", 56 | "message": "Old Vault Secret Source > Type", 57 | "choices": VAULT_TYPES, 58 | }, 59 | { 60 | "type": "input", 61 | "name": "old_vault.value.plain_text", 62 | "message": "Old Vault Secret Source > Value (Text)", 63 | "when": when_type("old", VAULT_TYPE_PLAIN_TEXT), 64 | "validate": validate_present, 65 | "invalid_message": "Old vault passphrase needs to be set", 66 | }, 67 | { 68 | "type": "filepath", 69 | "name": "old_vault.value.file", 70 | "message": "Old Vault Secret Source > Value (File)", 71 | "when": when_type("old", VAULT_TYPE_FILE), 72 | "validate": validate_directory, 73 | }, 74 | { 75 | "type": "list", 76 | "name": "new_vault.type", 77 | "message": "New Vault Secret Source > Type", 78 | "choices": VAULT_TYPES, 79 | }, 80 | { 81 | "type": "input", 82 | "name": "new_vault.value.plain_text", 83 | "message": "Old Vault Secret Source > Value (Text)", 84 | "when": when_type("new", VAULT_TYPE_PLAIN_TEXT), 85 | "validate": validate_present, 86 | "invalid_message": "New vault passphrase needs to be set", 87 | }, 88 | { 89 | "type": "filepath", 90 | "name": "new_vault.value.file", 91 | "message": "New Vault Secret Source > Value (File)", 92 | "when": when_type("new", "file"), 93 | "validate": validate_directory, 94 | }, 95 | { 96 | "type": "filepath", 97 | "name": "pwd", 98 | "message": "Working directory for execution, this also changes the relative path for file urls", 99 | "default": getcwd(), 100 | }, 101 | { 102 | "type": "confirm", 103 | "name": "ignore_errors", 104 | "message": "Should we ignore when an error occurs processing individual files?", 105 | }, 106 | { 107 | "type": "confirm", 108 | "name": "update_source_secret", 109 | "message": "Should the source secret be updated?", 110 | "when": when_type("old", VAULT_TYPE_FILE), 111 | }, 112 | ] 113 | 114 | 115 | def prompt_tui() -> dict[str]: 116 | args = prompt(questions) 117 | remap_vault_source(args, "old") 118 | remap_vault_source(args, "new") 119 | return args 120 | -------------------------------------------------------------------------------- /ansible_vault_rotate/cli/tui_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .tui import validate_present, when_type, remap_vault_source 3 | 4 | class TuiTest(unittest.TestCase): 5 | def test_validate_present(self): 6 | self.assertFalse(validate_present("")) 7 | self.assertTrue(validate_present("test")) 8 | 9 | def test_when_type(self): 10 | callback = when_type("old","file") 11 | self.assertTrue(callback({ 12 | "old_vault.type": "file" 13 | })) 14 | 15 | self.assertFalse(callback({ 16 | "old_vault.type": "plain text" 17 | })) 18 | 19 | def test_remap_vault_source(self): 20 | args = { 21 | "old_vault.type": "file", 22 | "old_vault.value.file": "vault.txt" 23 | } 24 | remap_vault_source(args, "old") 25 | print(args) 26 | self.assertIn("old_vault_secret_source", args) 27 | self.assertEqual(args['old_vault_secret_source'], "file://vault.txt") 28 | -------------------------------------------------------------------------------- /ansible_vault_rotate/match/__init__.py: -------------------------------------------------------------------------------- 1 | from .find import find_vault_strings, FindVaultStringResult 2 | -------------------------------------------------------------------------------- /ansible_vault_rotate/match/__testdata__/label_vaulted.yml: -------------------------------------------------------------------------------- 1 | test: !vault | 2 | $ANSIBLE_VAULT;1.2;AES256;dev 3 | 61323931353866666336306139373937316366366138656131323863373866376666353364373761 4 | 3539633234313836346435323766306164626134376564330a373530313635343535343133316133 5 | 36643666306434616266376434363239346433643238336464643566386135356334303736353136 6 | 6565633133366366360a326566323363363936613664616364623437336130623133343530333739 7 | 3039 8 | regular_key: goes here 9 | -------------------------------------------------------------------------------- /ansible_vault_rotate/match/__testdata__/nested_vaulted.yml: -------------------------------------------------------------------------------- 1 | nested: 2 | test: !vault | 3 | $ANSIBLE_VAULT;1.2;AES256;dev 4 | 61323931353866666336306139373937316366366138656131323863373866376666353364373761 5 | 3539633234313836346435323766306164626134376564330a373530313635343535343133316133 6 | 36643666306434616266376434363239346433643238336464643566386135356334303736353136 7 | 6565633133366366360a326566323363363936613664616364623437336130623133343530333739 8 | 3039 9 | regular_key: goes here 10 | -------------------------------------------------------------------------------- /ansible_vault_rotate/match/__testdata__/single_vaulted.yml: -------------------------------------------------------------------------------- 1 | test: !vault | 2 | $ANSIBLE_VAULT;1.1;AES256 3 | 34636530313034373261383234633232653732316262383339653836323862306263613432623935 4 | 6536646366356261386539343166333065356432663264650a313566316439356364663032346639 5 | 64396563353261333239643163303933343265666433666632333535336565313331613863383936 6 | 6662356434666238370a346334643536653462333164643464383233623830393766333561316538 7 | 3333 8 | regular_key: goes here 9 | -------------------------------------------------------------------------------- /ansible_vault_rotate/match/__testdata__/single_vaulted_eof.yml: -------------------------------------------------------------------------------- 1 | test: !vault | 2 | $ANSIBLE_VAULT;1.1;AES256 3 | 34636530313034373261383234633232653732316262383339653836323862306263613432623935 4 | 6536646366356261386539343166333065356432663264650a313566316439356364663032346639 5 | 64396563353261333239643163303933343265666433666632333535336565313331613863383936 6 | 6662356434666238370a346334643536653462333164643464383233623830393766333561316538 7 | 3333 -------------------------------------------------------------------------------- /ansible_vault_rotate/match/find.py: -------------------------------------------------------------------------------- 1 | import re 2 | import typing 3 | 4 | ANSIBLE_VAULT_REGEX = re.compile(r'(^(\s*)\$ANSIBLE_VAULT;(\S*)\n(\s*\w+$)*)', re.MULTILINE) 5 | 6 | 7 | class FindVaultStringResult(typing.TypedDict): 8 | """ 9 | Representation for vaulted string occurence 10 | """ 11 | vaultedString: str 12 | label: typing.Union[str, None] 13 | indent: str 14 | 15 | 16 | def find_vault_strings(file_path: str) -> typing.Generator[FindVaultStringResult, None, None]: 17 | """ 18 | Find all vaulted strings in a given file 19 | :param file_path: Path to file to search for 20 | :return: All occurrences of vaulted strings in the given file 21 | """ 22 | with open(file_path, "r") as f: 23 | content = "".join(f.readlines()) 24 | for match in ANSIBLE_VAULT_REGEX.findall(content): 25 | meta = match[2].split(";") 26 | 27 | yield FindVaultStringResult( 28 | vaultedString=match[0], 29 | label=meta[2] if len(meta) == 3 else None, 30 | indent=match[1] 31 | ) 32 | -------------------------------------------------------------------------------- /ansible_vault_rotate/match/test_find.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from .find import find_vault_strings, FindVaultStringResult 5 | 6 | 7 | class MatchFindTest(unittest.TestCase): 8 | 9 | def load_results(self, file_name: str) -> list[FindVaultStringResult]: 10 | return [item for item in 11 | find_vault_strings(f"{os.path.dirname(os.path.abspath(__file__))}/__testdata__/{file_name}.yml")] 12 | 13 | def test_find_single(self): 14 | results = self.load_results("single_vaulted") 15 | self.assertEqual(len(results), 1) 16 | 17 | result = results[0] 18 | self.assertIsNone(result['label']) 19 | self.assertEqual(result['indent'], ' ') 20 | 21 | def test_find_single_eof(self): 22 | results = self.load_results("single_vaulted_eof") 23 | self.assertEqual(len(results), 1) 24 | 25 | result = results[0] 26 | self.assertIsNone(result['label']) 27 | self.assertEqual(result['indent'], ' ') 28 | 29 | def test_find_labeled(self): 30 | results = self.load_results("label_vaulted") 31 | self.assertEqual(len(results), 1) 32 | 33 | result = results[0] 34 | self.assertEqual(result['label'], "dev") 35 | self.assertEqual(result['indent'], ' ') 36 | 37 | def test_find_nested(self): 38 | results = self.load_results("nested_vaulted") 39 | self.assertEqual(len(results), 1) 40 | 41 | result = results[0] 42 | self.assertEqual(result['label'], "dev") 43 | self.assertEqual(result['indent'], ' ') 44 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/__init__.py: -------------------------------------------------------------------------------- 1 | from .string import vault_string 2 | from .file import rekey_file 3 | from .util import load_with_vault 4 | from .vault_source import build_vault_source 5 | from .detect import has_vault_secrets 6 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/__testdata__/TEST: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/__testdata__/multiple-secret.yml: -------------------------------------------------------------------------------- 1 | my_key: 2 | a: !vault | 3 | $ANSIBLE_VAULT;1.1;AES256 4 | 31663031633532633662666465396235383461646561373762303432373866313466376637303764 5 | 3734363362623037613935326332623039636434373562300a336639313131326135323833346634 6 | 39343737336437333161656434613064376633366435383663643836316135393336393932386530 7 | 3965643937663235320a353463623430373333373337636339313238633931343932313166363663 8 | 6161 9 | b: !vault | 10 | $ANSIBLE_VAULT;1.1;AES256 11 | 63643133383966373836303436363734373033636533376564303030376166366235653334323039 12 | 6632633666653634313865666134383731333236653365300a653935376331653030376634666666 13 | 34336234636233316464306633626262306263653966623935396566626461366637633931353239 14 | 6263633237323735630a623739336465353932326465343763346238626537343163326165353337 15 | 3534 16 | 17 | regular_key: goes here 18 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/__testdata__/multiple-vault.yml: -------------------------------------------------------------------------------- 1 | my_key: 2 | a: !vault | 3 | $ANSIBLE_VAULT;1.2;AES256;dev 4 | 31663031633532633662666465396235383461646561373762303432373866313466376637303764 5 | 3734363362623037613935326332623039636434373562300a336639313131326135323833346634 6 | 39343737336437333161656434613064376633366435383663643836316135393336393932386530 7 | 3965643937663235320a353463623430373333373337636339313238633931343932313166363663 8 | 6161 9 | b: !vault | 10 | $ANSIBLE_VAULT;1.2;AES256;prod 11 | 63363031323962313139323162396630306138353162353261343535613461613734363539316464 12 | 3365386261643737653361326239626530663533343362630a336133656166383031363834633930 13 | 61636231666663666465303161643138316636363639353530396664306139303831623437346430 14 | 3734663734613339300a323330313033366638363438326462353931326530633939636531343364 15 | 3139 16 | 17 | regular_key: goes here 18 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/__testdata__/single-secret-eof.yml: -------------------------------------------------------------------------------- 1 | test: !vault | 2 | $ANSIBLE_VAULT;1.1;AES256 3 | 31663031633532633662666465396235383461646561373762303432373866313466376637303764 4 | 3734363362623037613935326332623039636434373562300a336639313131326135323833346634 5 | 39343737336437333161656434613064376633366435383663643836316135393336393932386530 6 | 3965643937663235320a353463623430373333373337636339313238633931343932313166363663 7 | 6161 -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/__testdata__/single-secret.yml: -------------------------------------------------------------------------------- 1 | test: !vault | 2 | $ANSIBLE_VAULT;1.1;AES256 3 | 31663031633532633662666465396235383461646561373762303432373866313466376637303764 4 | 3734363362623037613935326332623039636434373562300a336639313131326135323833346634 5 | 39343737336437333161656434613064376633366435383663643836316135393336393932386530 6 | 3965643937663235320a353463623430373333373337636339313238633931343932313166363663 7 | 6161 8 | regular_key: goes here 9 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/__testdata__/vault-password: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/__testdata__/vaulted-file.txt: -------------------------------------------------------------------------------- 1 | $ANSIBLE_VAULT;1.1;AES256 2 | 37326561656264346232363334373839626531663762303330656330313162613063326537363661 3 | 6564356163616434636161313239613766346433373262650a306636616162623935306236643533 4 | 63333933386364646633613331666430636631613361376632656231393364393130363534386339 5 | 6538633165373532370a323333363033323730316464636536353734636131323832326536333131 6 | 64346536636635613233303037653262346335306137346639353933376530313235 7 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/detect.py: -------------------------------------------------------------------------------- 1 | def has_vault_secrets(path: str): 2 | found = False 3 | with open(path, "r") as f: 4 | line = f.readline() 5 | while line: 6 | if line.startswith("$ANSIBLE_VAULT") or '$ANSIBLE_VAULT' in line: 7 | found = True 8 | break 9 | line = f.readline() 10 | 11 | return found 12 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/detect_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from .detect import has_vault_secrets 4 | 5 | 6 | class DetectTest(unittest.TestCase): 7 | def fixture_name(self, file_name: str) -> str: 8 | return os.path.dirname(os.path.abspath(__file__)) + f"/__testdata__/{file_name}" 9 | 10 | def test_vault_file(self): 11 | self.assertTrue(has_vault_secrets(self.fixture_name("vaulted-file.txt"))) 12 | 13 | def test_inline_secrets(self): 14 | self.assertTrue(has_vault_secrets(self.fixture_name("multiple-secret.yml"))) 15 | 16 | def test_no_match(self): 17 | self.assertFalse(has_vault_secrets(self.fixture_name("vault-password"))) 18 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/file.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from ansible.errors import AnsibleError 4 | 5 | from .string import vault_string 6 | from ansible_vault_rotate.match import find_vault_strings 7 | 8 | 9 | def rekey_file(source: str, old_passphrase: str, new_passphrase: str, target: typing.Union[str, None] = None) -> int: 10 | """ 11 | Rekey all ansible vault secrets in the given file 12 | :param source: Path to source file 13 | :param old_passphrase: Old passphrase to decrypt secrets 14 | :param new_passphrase: New passphrase to encrypt secrets with 15 | :param target: Target path, if not specified overwrites source path 16 | :return: Amount of replaced secrets 17 | """ 18 | replacements = [] 19 | for match in find_vault_strings(source): 20 | try: 21 | replacement = vault_string(match, old_passphrase, new_passphrase) 22 | replacements.append((match, replacement)) 23 | except AnsibleError as e: 24 | # Ignore decryption errors 25 | if "Decryption failed" not in str(e): 26 | raise e 27 | if len(replacements) == 0: 28 | raise AnsibleError("Decryption failed (no vault secrets were found that could decrypt)") 29 | 30 | with open(source, "r") as file_to_rekey: 31 | file_content = file_to_rekey.read() 32 | 33 | if target is None: 34 | target = source 35 | 36 | with open(target, "w") as file_to_rekey: 37 | for match, replacement in replacements: 38 | file_content = file_content.replace(match['vaultedString'], replacement) 39 | 40 | file_to_rekey.write(file_content) 41 | 42 | return len(replacements) 43 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/file_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from tempfile import NamedTemporaryFile 4 | 5 | from ansible.parsing.vault import VaultEditor 6 | from ansible.parsing.dataloader import DataLoader 7 | 8 | from ansible_vault_rotate.vault import load_with_vault 9 | from ansible_vault_rotate.vault.util import create_vault_secret 10 | from .file import rekey_file 11 | from .util import create_vault_lib 12 | 13 | 14 | class VaultFileTest(unittest.TestCase): 15 | def fixture_name(self, file_name: str) -> str: 16 | return os.path.dirname(os.path.abspath(__file__)) + f"/__testdata__/{file_name}" 17 | 18 | def assertLineCount(self, f: NamedTemporaryFile, count: int): 19 | lines = f.readlines() 20 | self.assertEqual(len(lines), count) 21 | f.seek(0) 22 | 23 | def test_single_secret(self): 24 | with NamedTemporaryFile("r", delete=False) as f: 25 | rekey_file(self.fixture_name("single-secret.yml"), "test", "test123", f.name) 26 | 27 | self.assertLineCount(f, 8) 28 | 29 | os.chdir("/tmp") # work around for ansible path resolve issues 30 | doc = load_with_vault(f.name, "default", "test123") 31 | self.assertEqual(doc['regular_key'], "goes here") 32 | self.assertEqual(doc['test'], 'test') 33 | 34 | def test_single_secret_eof(self): 35 | with NamedTemporaryFile("r", delete=False) as f: 36 | rekey_file(self.fixture_name("single-secret-eof.yml"), "test", "test123", f.name) 37 | 38 | self.assertLineCount(f, 7) 39 | 40 | os.chdir("/tmp") # work around for ansible path resolve issues 41 | doc = load_with_vault(f.name, "default", "test123") 42 | self.assertEqual(doc['test'], 'test') 43 | 44 | def test_multiple_secret(self): 45 | with NamedTemporaryFile("r", delete=False) as f: 46 | rekey_file(self.fixture_name("multiple-secret.yml"), "test", "test123", f.name) 47 | 48 | self.assertLineCount(f, 17) 49 | 50 | os.chdir("/tmp") # work around for ansible path resolve issues 51 | doc = load_with_vault(f.name, "default", "test123") 52 | self.assertEqual(doc['regular_key'], "goes here") 53 | self.assertEqual(doc['my_key']['a'], 'test') 54 | self.assertEqual(doc['my_key']['b'], 'abc') 55 | 56 | def test_vault_file(self): 57 | with NamedTemporaryFile("r", delete=True) as f: 58 | rekey_file(self.fixture_name("vaulted-file.txt"), "test", "test123", f.name) 59 | 60 | self.assertLineCount(f, 6) 61 | 62 | editor = VaultEditor(create_vault_lib("default", "test123")) 63 | content = editor.plaintext(f.name).decode("utf8") 64 | self.assertEqual(content, "i am\na multiline\nstring\n") 65 | 66 | def test_multiple_vault(self): 67 | with NamedTemporaryFile("r", delete=True) as f: 68 | rekey_file(self.fixture_name("multiple-vault.yml"), "testprod", "testprod123", f.name) 69 | 70 | self.assertLineCount(f, 17) 71 | os.chdir("/tmp") # work around for ansible path resolve issues 72 | 73 | loader = DataLoader() 74 | loader.set_vault_secrets(([ 75 | ("dev", create_vault_secret("test")), 76 | ("prod", create_vault_secret("testprod123")), 77 | ])) 78 | doc = loader.load_from_file(f.name) 79 | self.assertEqual(doc['regular_key'], "goes here") 80 | self.assertEqual(doc['my_key']['a'], 'test') 81 | self.assertEqual(doc['my_key']['b'], 'abc') 82 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/string.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from tempfile import NamedTemporaryFile 3 | from ansible.parsing.vault import VaultEditor, VaultLib, VaultSecret 4 | from ansible_vault_rotate.match import FindVaultStringResult 5 | from os import remove 6 | from .util import create_vault_lib, create_vault_secret 7 | 8 | 9 | def vault_string(vault_string_search_result: FindVaultStringResult, old_passphrase: str, new_passphrase: str) -> str: 10 | """ 11 | Rekey a given vault string search result by decrypting the passphrase and rekeying with new passphrase 12 | 13 | Secret data is never written to disk in the process 14 | 15 | :param vault_string_search_result: Details about the vaulted string 16 | :param old_passphrase: Old passphrase to decrypt 17 | :param new_passphrase: New passphrase to encrypt the secret in after decryption 18 | :return: Indented and with new passphrase vaulted string 19 | """ 20 | indent = vault_string_search_result['indent'] 21 | vaulted_string = vault_string_search_result['vaultedString'] 22 | label = vault_string_search_result['label'] 23 | 24 | # write vaulted text to file 25 | with NamedTemporaryFile(mode='w', delete=False) as f: 26 | f.write(vaulted_string.replace(indent, '')) 27 | temp_file = f 28 | 29 | if temp_file is None: 30 | raise IOError("Could not create temporary file for rekey") 31 | 32 | # rekey file 33 | editor = VaultEditor(create_vault_lib(label, old_passphrase)) 34 | editor.rekey_file(temp_file.name, create_vault_secret(new_passphrase), label) 35 | 36 | # read content and add indentation again 37 | with open(f.name, "r") as f: 38 | new_vault = indent + indent.join(f.readlines()).rstrip() 39 | content = vaulted_string.replace(vaulted_string, new_vault) 40 | 41 | # delete temp file and return ready to use string 42 | remove(f.name) 43 | return content 44 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/string_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .string import vault_string 3 | from ansible_vault_rotate.match import FindVaultStringResult 4 | 5 | 6 | class VaultStringTest(unittest.TestCase): 7 | def verify_indent(self, result, indent): 8 | lines = result.split("\n") 9 | for line in lines: 10 | if line == "": 11 | continue 12 | self.assertTrue(line.startswith(indent), "line '%s' is not indented" % line) 13 | self.assertEqual(len(lines), 6) 14 | 15 | def test_rekey_unlabeled(self): 16 | search_result = FindVaultStringResult( 17 | vaultedString=""" $ANSIBLE_VAULT;1.1;AES256 18 | 34636530313034373261383234633232653732316262383339653836323862306263613432623935 19 | 6536646366356261386539343166333065356432663264650a313566316439356364663032346639 20 | 64396563353261333239643163303933343265666433666632333535336565313331613863383936 21 | 6662356434666238370a346334643536653462333164643464383233623830393766333561316538 22 | 3333""", 23 | indent=' ', 24 | label=None) 25 | 26 | result = vault_string(search_result, "test", "test123") 27 | self.assertIsNotNone(result) 28 | self.verify_indent(result, ' ') 29 | 30 | def test_rekey_labeled(self): 31 | search_result = FindVaultStringResult( 32 | vaultedString=""" $ANSIBLE_VAULT;1.1;AES256;dev 33 | 34636530313034373261383234633232653732316262383339653836323862306263613432623935 34 | 6536646366356261386539343166333065356432663264650a313566316439356364663032346639 35 | 64396563353261333239643163303933343265666433666632333535336565313331613863383936 36 | 6662356434666238370a346334643536653462333164643464383233623830393766333561316538 37 | 3333""", 38 | indent=' ', 39 | label='dev') 40 | 41 | result = vault_string(search_result, "test", "test123") 42 | self.assertIn('dev', result) 43 | self.assertIsNotNone(result) 44 | self.verify_indent(result, ' ') 45 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/util.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from ansible.parsing.vault import VaultLib, VaultSecret 4 | from ansible.parsing.dataloader import DataLoader 5 | 6 | 7 | def create_vault_secret(passphrase: str) -> VaultSecret: 8 | return VaultSecret(passphrase.encode()) 9 | 10 | 11 | def create_vault_lib(label: typing.Union[str, None], passphrase: str) -> VaultLib: 12 | return VaultLib([ 13 | (label if label is not None else "default", create_vault_secret(passphrase)) 14 | ]) 15 | 16 | 17 | def load_with_vault(path: str, vault_label: str, passphrase: str) -> dict: 18 | loader = DataLoader() 19 | loader.set_vault_secrets(([(vault_label, create_vault_secret(passphrase))])) 20 | return loader.load_from_file(path) 21 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/vault_source.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class VaultSource(ABC): 5 | """ 6 | Implement this method to enable a different source for vault secrets 7 | """ 8 | 9 | def __init__(self, source: str): 10 | self.source = source 11 | 12 | @abstractmethod 13 | def read(self) -> str: 14 | pass 15 | 16 | @abstractmethod 17 | def write(self, value: str) -> None: 18 | pass 19 | 20 | 21 | class FileVaultSource(VaultSource): 22 | """ 23 | Implementation of VaultSource to load secret from file 24 | """ 25 | 26 | def __open_file(self, mode: str): 27 | return open(self.source.replace("file://", ""), mode) 28 | 29 | def read(self) -> str: 30 | with self.__open_file("r") as f: 31 | return f.read().rstrip() 32 | 33 | def write(self, value: str) -> None: 34 | with self.__open_file("w") as f: 35 | f.write(value) 36 | 37 | 38 | class TextVaultSource(VaultSource): 39 | """ 40 | Implementation of VaultSource to load secret from given text 41 | """ 42 | 43 | def read(self) -> str: 44 | return self.source 45 | 46 | def write(self, value: str) -> None: 47 | # noop 48 | pass 49 | 50 | 51 | def build_vault_source(raw: str) -> VaultSource: 52 | """ 53 | Construct correct VaultSource based on input provided 54 | :param raw: Raw text 55 | """ 56 | if raw.startswith("file://"): 57 | return FileVaultSource(raw) 58 | else: 59 | return TextVaultSource(raw) 60 | -------------------------------------------------------------------------------- /ansible_vault_rotate/vault/vault_source_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from tempfile import NamedTemporaryFile 4 | 5 | from .vault_source import TextVaultSource, FileVaultSource, build_vault_source 6 | 7 | 8 | class VaultSourceTest(unittest.TestCase): 9 | def fixture_name(self, file_name: str) -> str: 10 | return os.path.dirname(os.path.abspath(__file__)) + f"/__testdata__/{file_name}" 11 | 12 | def test_text_source(self): 13 | text_vault = TextVaultSource("text") 14 | self.assertEqual(text_vault.read(), "text") 15 | 16 | def test_file_source(self): 17 | file_vault = FileVaultSource(f"file://{self.fixture_name('vault-password')}") 18 | self.assertEqual(file_vault.read(), "test") 19 | 20 | with NamedTemporaryFile("w") as f: 21 | f.write("test") 22 | file_vault = FileVaultSource(f"file://{f.name}") 23 | file_vault.write("updated") 24 | with open(f.name) as f: 25 | self.assertEqual(f.read(), "updated") 26 | 27 | def test_build_text(self): 28 | source = build_vault_source("test") 29 | self.assertIsInstance(source, TextVaultSource) 30 | 31 | def test_build_file(self): 32 | source = build_vault_source("file://test.txt") 33 | self.assertIsInstance(source, FileVaultSource) 34 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "50...75" 5 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "ansible-core" 5 | version = "2.15.13" 6 | description = "Radically simple IT automation" 7 | optional = false 8 | python-versions = ">=3.9" 9 | files = [ 10 | {file = "ansible_core-2.15.13-py3-none-any.whl", hash = "sha256:e7f50bbb61beae792f5ecb86eff82149d3948d078361d70aedb01d76bc483c30"}, 11 | {file = "ansible_core-2.15.13.tar.gz", hash = "sha256:f542e702ee31fb049732143aeee6b36311ca48b7d13960a0685afffa0d742d7f"}, 12 | ] 13 | 14 | [package.dependencies] 15 | cryptography = "*" 16 | importlib-resources = {version = ">=5.0,<5.1", markers = "python_version < \"3.10\""} 17 | jinja2 = ">=3.0.0" 18 | packaging = "*" 19 | PyYAML = ">=5.1" 20 | resolvelib = ">=0.5.3,<1.1.0" 21 | 22 | [[package]] 23 | name = "cffi" 24 | version = "1.17.1" 25 | description = "Foreign Function Interface for Python calling C code." 26 | optional = false 27 | python-versions = ">=3.8" 28 | files = [ 29 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 30 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 31 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 32 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 33 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 34 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 35 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 36 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 37 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 38 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 39 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 40 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 41 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 42 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 43 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 44 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 45 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 46 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 47 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 48 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 49 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 50 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 51 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 52 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 53 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 54 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 55 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 56 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 57 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 58 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 59 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 60 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 61 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 62 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 63 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 64 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 65 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 66 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 67 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 68 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 69 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 70 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 71 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 72 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 73 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 74 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 75 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 76 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 77 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 78 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 79 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 80 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 81 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 82 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 83 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 84 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 85 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 86 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 87 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 88 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 89 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 90 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 91 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 92 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 93 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 94 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 95 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 96 | ] 97 | 98 | [package.dependencies] 99 | pycparser = "*" 100 | 101 | [[package]] 102 | name = "colorama" 103 | version = "0.4.6" 104 | description = "Cross-platform colored terminal text." 105 | optional = false 106 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 107 | files = [ 108 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 109 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 110 | ] 111 | 112 | [[package]] 113 | name = "coverage" 114 | version = "7.6.9" 115 | description = "Code coverage measurement for Python" 116 | optional = false 117 | python-versions = ">=3.9" 118 | files = [ 119 | {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, 120 | {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, 121 | {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, 122 | {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, 123 | {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, 124 | {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, 125 | {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, 126 | {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, 127 | {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, 128 | {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, 129 | {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, 130 | {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, 131 | {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, 132 | {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, 133 | {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, 134 | {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, 135 | {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, 136 | {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, 137 | {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, 138 | {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, 139 | {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, 140 | {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, 141 | {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, 142 | {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, 143 | {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, 144 | {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, 145 | {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, 146 | {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, 147 | {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, 148 | {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, 149 | {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, 150 | {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, 151 | {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, 152 | {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, 153 | {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, 154 | {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, 155 | {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, 156 | {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, 157 | {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, 158 | {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, 159 | {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, 160 | {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, 161 | {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, 162 | {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, 163 | {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, 164 | {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, 165 | {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, 166 | {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, 167 | {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, 168 | {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, 169 | {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, 170 | {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, 171 | {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, 172 | {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, 173 | {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, 174 | {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, 175 | {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, 176 | {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, 177 | {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, 178 | {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, 179 | {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, 180 | {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, 181 | ] 182 | 183 | [package.extras] 184 | toml = ["tomli"] 185 | 186 | [[package]] 187 | name = "cryptography" 188 | version = "43.0.3" 189 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 190 | optional = false 191 | python-versions = ">=3.7" 192 | files = [ 193 | {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, 194 | {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, 195 | {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, 196 | {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, 197 | {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, 198 | {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, 199 | {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, 200 | {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, 201 | {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, 202 | {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, 203 | {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, 204 | {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, 205 | {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, 206 | {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, 207 | {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, 208 | {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, 209 | {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, 210 | {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, 211 | {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, 212 | {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, 213 | {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, 214 | {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, 215 | {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, 216 | {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, 217 | {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, 218 | {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, 219 | {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, 220 | ] 221 | 222 | [package.dependencies] 223 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 224 | 225 | [package.extras] 226 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 227 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 228 | nox = ["nox"] 229 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 230 | sdist = ["build"] 231 | ssh = ["bcrypt (>=3.1.5)"] 232 | test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 233 | test-randomorder = ["pytest-randomly"] 234 | 235 | [[package]] 236 | name = "exceptiongroup" 237 | version = "1.2.2" 238 | description = "Backport of PEP 654 (exception groups)" 239 | optional = false 240 | python-versions = ">=3.7" 241 | files = [ 242 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 243 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 244 | ] 245 | 246 | [package.extras] 247 | test = ["pytest (>=6)"] 248 | 249 | [[package]] 250 | name = "importlib-resources" 251 | version = "5.0.7" 252 | description = "Read resources from Python packages" 253 | optional = false 254 | python-versions = ">=3.6" 255 | files = [ 256 | {file = "importlib_resources-5.0.7-py3-none-any.whl", hash = "sha256:2238159eb743bd85304a16e0536048b3e991c531d1cd51c4a834d1ccf2829057"}, 257 | {file = "importlib_resources-5.0.7.tar.gz", hash = "sha256:4df460394562b4581bb4e4087ad9447bd433148fba44241754ec3152499f1d1b"}, 258 | ] 259 | 260 | [package.extras] 261 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 262 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler", "pytest-flake8", "pytest-mypy"] 263 | 264 | [[package]] 265 | name = "iniconfig" 266 | version = "2.0.0" 267 | description = "brain-dead simple config-ini parsing" 268 | optional = false 269 | python-versions = ">=3.7" 270 | files = [ 271 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 272 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 273 | ] 274 | 275 | [[package]] 276 | name = "inquirerpy" 277 | version = "0.3.4" 278 | description = "Python port of Inquirer.js (A collection of common interactive command-line user interfaces)" 279 | optional = false 280 | python-versions = ">=3.7,<4.0" 281 | files = [ 282 | {file = "InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4"}, 283 | {file = "InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e"}, 284 | ] 285 | 286 | [package.dependencies] 287 | pfzy = ">=0.3.1,<0.4.0" 288 | prompt-toolkit = ">=3.0.1,<4.0.0" 289 | 290 | [package.extras] 291 | docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] 292 | 293 | [[package]] 294 | name = "jinja2" 295 | version = "3.1.4" 296 | description = "A very fast and expressive template engine." 297 | optional = false 298 | python-versions = ">=3.7" 299 | files = [ 300 | {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, 301 | {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, 302 | ] 303 | 304 | [package.dependencies] 305 | MarkupSafe = ">=2.0" 306 | 307 | [package.extras] 308 | i18n = ["Babel (>=2.7)"] 309 | 310 | [[package]] 311 | name = "markupsafe" 312 | version = "3.0.2" 313 | description = "Safely add untrusted strings to HTML/XML markup." 314 | optional = false 315 | python-versions = ">=3.9" 316 | files = [ 317 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 318 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 319 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 320 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 321 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 322 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 323 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 324 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 325 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 326 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 327 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 328 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 329 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 330 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 331 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 332 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 333 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 334 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 335 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 336 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 337 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 338 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 339 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 340 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 341 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 342 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 343 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 344 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 345 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 346 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 347 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 348 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 349 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 350 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 351 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 352 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 353 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 354 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 355 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 356 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 357 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 358 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 359 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 360 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 361 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 362 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 363 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 364 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 365 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 366 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 367 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 368 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 369 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 370 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 371 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 372 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 373 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 374 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 375 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 376 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 377 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 378 | ] 379 | 380 | [[package]] 381 | name = "packaging" 382 | version = "24.2" 383 | description = "Core utilities for Python packages" 384 | optional = false 385 | python-versions = ">=3.8" 386 | files = [ 387 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 388 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 389 | ] 390 | 391 | [[package]] 392 | name = "pfzy" 393 | version = "0.3.4" 394 | description = "Python port of the fzy fuzzy string matching algorithm" 395 | optional = false 396 | python-versions = ">=3.7,<4.0" 397 | files = [ 398 | {file = "pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96"}, 399 | {file = "pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1"}, 400 | ] 401 | 402 | [package.extras] 403 | docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] 404 | 405 | [[package]] 406 | name = "pluggy" 407 | version = "1.5.0" 408 | description = "plugin and hook calling mechanisms for python" 409 | optional = false 410 | python-versions = ">=3.8" 411 | files = [ 412 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 413 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 414 | ] 415 | 416 | [package.extras] 417 | dev = ["pre-commit", "tox"] 418 | testing = ["pytest", "pytest-benchmark"] 419 | 420 | [[package]] 421 | name = "prompt-toolkit" 422 | version = "3.0.48" 423 | description = "Library for building powerful interactive command lines in Python" 424 | optional = false 425 | python-versions = ">=3.7.0" 426 | files = [ 427 | {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, 428 | {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, 429 | ] 430 | 431 | [package.dependencies] 432 | wcwidth = "*" 433 | 434 | [[package]] 435 | name = "pycparser" 436 | version = "2.22" 437 | description = "C parser in Python" 438 | optional = false 439 | python-versions = ">=3.8" 440 | files = [ 441 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 442 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 443 | ] 444 | 445 | [[package]] 446 | name = "pytest" 447 | version = "7.4.4" 448 | description = "pytest: simple powerful testing with Python" 449 | optional = false 450 | python-versions = ">=3.7" 451 | files = [ 452 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 453 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 454 | ] 455 | 456 | [package.dependencies] 457 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 458 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 459 | iniconfig = "*" 460 | packaging = "*" 461 | pluggy = ">=0.12,<2.0" 462 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 463 | 464 | [package.extras] 465 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 466 | 467 | [[package]] 468 | name = "pyyaml" 469 | version = "6.0.2" 470 | description = "YAML parser and emitter for Python" 471 | optional = false 472 | python-versions = ">=3.8" 473 | files = [ 474 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 475 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 476 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 477 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 478 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 479 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 480 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 481 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 482 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 483 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 484 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 485 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 486 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 487 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 488 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 489 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 490 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 491 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 492 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 493 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 494 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 495 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 496 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 497 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 498 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 499 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 500 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 501 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 502 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 503 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 504 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 505 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 506 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 507 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 508 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 509 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 510 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 511 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 512 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 513 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 514 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 515 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 516 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 517 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 518 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 519 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 520 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 521 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 522 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 523 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 524 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 525 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 526 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 527 | ] 528 | 529 | [[package]] 530 | name = "resolvelib" 531 | version = "1.0.1" 532 | description = "Resolve abstract dependencies into concrete ones" 533 | optional = false 534 | python-versions = "*" 535 | files = [ 536 | {file = "resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf"}, 537 | {file = "resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309"}, 538 | ] 539 | 540 | [package.extras] 541 | examples = ["html5lib", "packaging", "pygraphviz", "requests"] 542 | lint = ["black", "flake8", "isort", "mypy", "types-requests"] 543 | release = ["build", "towncrier", "twine"] 544 | test = ["commentjson", "packaging", "pytest"] 545 | 546 | [[package]] 547 | name = "tomli" 548 | version = "2.2.1" 549 | description = "A lil' TOML parser" 550 | optional = false 551 | python-versions = ">=3.8" 552 | files = [ 553 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 554 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 555 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 556 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 557 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 558 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 559 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 560 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 561 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 562 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 563 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 564 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 565 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 566 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 567 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 568 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 569 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 570 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 571 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 572 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 573 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 574 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 575 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 576 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 577 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 578 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 579 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 580 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 581 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 582 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 583 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 584 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 585 | ] 586 | 587 | [[package]] 588 | name = "wcwidth" 589 | version = "0.2.13" 590 | description = "Measures the displayed width of unicode strings in a terminal" 591 | optional = false 592 | python-versions = "*" 593 | files = [ 594 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 595 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 596 | ] 597 | 598 | [metadata] 599 | lock-version = "2.0" 600 | python-versions = "^3.9" 601 | content-hash = "30d5ad0908ea61ca546cc2311e3a72842c8d0f693d12f25b911bd792f3ac4bb5" 602 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ansible-vault-rotate" 3 | version = "2.1.0" 4 | description = "Advanced Python CLI to rotate the secret used for ansible vault inline secrets and files in a project" 5 | authors = [ 6 | "Timo Reymann " 7 | ] 8 | license = "MIT" 9 | readme = "README.md" 10 | repository = "https://github.com/trustedshops-public/python-ansible-vault-rotate" 11 | packages = [ 12 | { include = "ansible_vault_rotate" } 13 | ] 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Developers", 17 | "Intended Audience :: System Administrators", 18 | "Environment :: Console", 19 | "Environment :: MacOS X", 20 | "Operating System :: POSIX", 21 | "Operating System :: Unix", 22 | "Framework :: Ansible" 23 | ] 24 | exclude = [ 25 | "**/*_test.py", 26 | "**/__testdata__/**" 27 | ] 28 | include = [ 29 | "LICENSE" 30 | ] 31 | 32 | [tool.poetry.dependencies] 33 | python = "^3.9" 34 | ansible-core = "^2.9.0" 35 | inquirerpy = "^0.3.4" 36 | 37 | [tool.poetry.scripts] 38 | ansible-vault-rotate = 'ansible_vault_rotate.cli.run:run' 39 | 40 | [tool.poetry.group.dev.dependencies] 41 | pytest = "^7.2.0" 42 | coverage = "^7.0.4" 43 | 44 | [tool.poetry.urls] 45 | "Bug Tracker" = "https://github.com/trustedshops-public/python-ansible-vault-rotate/issues" 46 | 47 | [[tool.poetry.source]] 48 | name = "testpypi" 49 | url = "https://test.pypi.org/legacy/" 50 | priority = "supplemental" 51 | 52 | [tool.coverage.run] 53 | omit = [".*", "*/site-packages/*", "*_test"] 54 | 55 | [tool.coverage.report] 56 | fail_under = 70 57 | 58 | [build-system] 59 | requires = ["poetry-core>=1.0.0"] 60 | build-backend = "poetry.core.masonry.api" 61 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>trustedshops-public/.github:renovate-config" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------