├── .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 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 | [](https://github.com/trustedshops-public/spring-boot-starter-keycloak-path-based-resolver/blob/main/LICENSE)
4 | [](https://pre-commit.com/)
5 | [](https://dl.circleci.com/status-badge/redirect/gh/trustedshops-public/python-ansible-vault-rotate/tree/main)
6 | [](https://pypi.org/project/ansible-vault-rotate)
7 | [](https://codecov.io/gh/trustedshops-public/python-ansible-vault-rotate)
8 | [](https://sonarcloud.io/summary/new_code?id=trustedshops-public_python-ansible-vault-rotate)
9 | [](https://sonarcloud.io/summary/new_code?id=trustedshops-public_python-ansible-vault-rotate)
10 | [](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 |
--------------------------------------------------------------------------------