├── pyventory ├── py.typed ├── __init__.py ├── errors.py ├── base.py ├── export.py └── assets.py ├── docs ├── .gitignore ├── how-tos │ ├── tbd.rst │ └── index.rst ├── guide │ ├── layout.rst │ ├── configuration.rst │ ├── structure.rst │ ├── installation.rst │ └── index.rst ├── tutorial │ ├── 01_first_steps.rst │ └── index.rst ├── Makefile ├── make.bat ├── index.rst └── conf.py ├── tests ├── example │ ├── .gitignore │ ├── inventory │ │ ├── project.py │ │ ├── __init__.py │ │ ├── base.py │ │ ├── env.py │ │ ├── prod.py │ │ ├── assets.py │ │ └── stages.py │ ├── ansible_hosts.py │ ├── terraform_vars.py │ ├── terraform.tf.json │ └── ansible.json ├── conftest.py ├── test_errors.py ├── test_asset.py ├── test_example.py └── test_inventory.py ├── .gitignore ├── setup.cfg ├── .github └── workflows │ └── ci.yml ├── .pre-commit-config.yaml ├── LICENSE ├── pyproject.toml ├── README.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── poetry.lock /pyventory/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | -------------------------------------------------------------------------------- /docs/how-tos/tbd.rst: -------------------------------------------------------------------------------- 1 | [TBD] 2 | ===== 3 | -------------------------------------------------------------------------------- /tests/example/.gitignore: -------------------------------------------------------------------------------- 1 | terraform_result* 2 | -------------------------------------------------------------------------------- /docs/guide/layout.rst: -------------------------------------------------------------------------------- 1 | Files layout 2 | ============ -------------------------------------------------------------------------------- /docs/tutorial/01_first_steps.rst: -------------------------------------------------------------------------------- 1 | First Steps 2 | =========== 3 | -------------------------------------------------------------------------------- /docs/guide/configuration.rst: -------------------------------------------------------------------------------- 1 | Configure Ansible 2 | ================= 3 | -------------------------------------------------------------------------------- /docs/guide/structure.rst: -------------------------------------------------------------------------------- 1 | Inventory structure 2 | =================== 3 | -------------------------------------------------------------------------------- /docs/guide/installation.rst: -------------------------------------------------------------------------------- 1 | Installing Pyventory 2 | ==================== 3 | -------------------------------------------------------------------------------- /docs/how-tos/index.rst: -------------------------------------------------------------------------------- 1 | How-tos 2 | ======= 3 | 4 | .. toctree:: 5 | 6 | tbd 7 | -------------------------------------------------------------------------------- /docs/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | Pyventory Tutorial 2 | ================== 3 | 4 | .. toctree:: 5 | 6 | 01_first_steps 7 | -------------------------------------------------------------------------------- /docs/guide/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | .. toctree:: 5 | 6 | installation 7 | structure 8 | layout 9 | configuration 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | *~ 4 | 5 | # dot files 6 | !/.github 7 | !/.pre-commit-config.yaml 8 | 9 | # python 10 | *.pyc 11 | /*.egg-info 12 | /dist 13 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope='session') 7 | def tests_dir(): 8 | return pathlib.Path(__file__).parent.absolute() 9 | -------------------------------------------------------------------------------- /tests/example/inventory/project.py: -------------------------------------------------------------------------------- 1 | from inventory import All 2 | 3 | 4 | class BackEnd(All): 5 | use_redis = True 6 | 7 | 8 | class FrontEnd(All): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/example/inventory/__init__.py: -------------------------------------------------------------------------------- 1 | from inventory.base import * 2 | from inventory.env import * 3 | from inventory.prod import * 4 | from inventory.project import * 5 | from inventory.stages import * 6 | -------------------------------------------------------------------------------- /tests/example/ansible_hosts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from inventory import assets 3 | 4 | from pyventory import ansible_inventory, terraform_vars 5 | 6 | 7 | ansible_inventory(hosts=vars(assets), indent=4) 8 | -------------------------------------------------------------------------------- /pyventory/__init__.py: -------------------------------------------------------------------------------- 1 | from pyventory.assets import Asset 2 | from pyventory.export import ansible_inventory, pyventory_data, terraform_vars 3 | 4 | 5 | __all__ = ['ansible_inventory', 'Asset', 'pyventory_data', 'terraform_vars'] 6 | -------------------------------------------------------------------------------- /tests/example/inventory/base.py: -------------------------------------------------------------------------------- 1 | from pyventory import Asset 2 | 3 | 4 | class All(Asset): 5 | run_tests = False 6 | use_redis = False 7 | redis_host = 'localhost' 8 | minify = False 9 | version = 'develop' 10 | -------------------------------------------------------------------------------- /tests/example/inventory/env.py: -------------------------------------------------------------------------------- 1 | from inventory import All 2 | 3 | 4 | class Staging(All): 5 | run_tests = True 6 | 7 | 8 | class Production(All): 9 | use_redis = True 10 | minify = True 11 | version = 'master' 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | disallow_untyped_defs = True 4 | disallow_incomplete_defs = True 5 | disallow_untyped_calls = True 6 | 7 | [flake8] 8 | ignore = E203, E266, E501, W503 9 | max-line-length = 88 10 | -------------------------------------------------------------------------------- /pyventory/errors.py: -------------------------------------------------------------------------------- 1 | class PyventoryError(Exception): 2 | pass 3 | 4 | 5 | class PropertyIsNotImplementedError(PyventoryError): 6 | pass 7 | 8 | 9 | class ValueSubstitutionError(PyventoryError): 10 | pass 11 | 12 | 13 | class ValueSubstitutionInfiniteLoopError(PyventoryError): 14 | pass 15 | -------------------------------------------------------------------------------- /tests/example/terraform_vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pathlib 3 | 4 | from inventory import assets 5 | 6 | from pyventory import terraform_vars 7 | 8 | 9 | terraform_vars( 10 | instances=vars(assets), 11 | filename_base=pathlib.Path(__file__).parent / 'terraform_result', 12 | indent=4, 13 | ) 14 | -------------------------------------------------------------------------------- /tests/example/inventory/prod.py: -------------------------------------------------------------------------------- 1 | from inventory.env import Production 2 | from inventory.project import BackEnd, FrontEnd 3 | 4 | 5 | class ProdBackEnd(Production, BackEnd): 6 | redis_host = 'prod_redis_hostname' 7 | ansible_hostname = 'app{num:03}.prod.dom' 8 | 9 | 10 | class ProdFrontEnd(Production, FrontEnd): 11 | ansible_hostname = 'www{num:03}.prod.dom' 12 | -------------------------------------------------------------------------------- /tests/example/inventory/assets.py: -------------------------------------------------------------------------------- 1 | from inventory import * 2 | 3 | 4 | develop = DevelopHost() 5 | develop_sidebranch = DevelopHost( 6 | ansible_host='sidebranch_hostname', version='sidebranch_name' 7 | ) 8 | 9 | staging = StagingHost() 10 | 11 | prod_backend1 = ProdBackEnd(num=1) 12 | prod_backend2 = ProdBackEnd(num=2) 13 | prod_frontend1 = ProdFrontEnd(num=1) 14 | prod_frontend2 = ProdFrontEnd(num=2) 15 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | from pyventory.errors import PyventoryError 2 | 3 | 4 | def test_pyventory_exception_format(): 5 | test = 'test' 6 | example = 'example' 7 | e = PyventoryError(f'This is {test} format string {example}') 8 | assert str(e) == 'This is test format string example' 9 | 10 | 11 | def test_pyventory_exception_format_degrade_on_non_string(): 12 | e = PyventoryError(42, 'test') 13 | assert str(e) == "(42, 'test')" 14 | -------------------------------------------------------------------------------- /tests/example/inventory/stages.py: -------------------------------------------------------------------------------- 1 | from inventory.env import Staging 2 | from inventory.project import BackEnd, FrontEnd 3 | 4 | 5 | class DevelopHost(Staging, BackEnd, FrontEnd): 6 | ansible_host = 'develop_hostname' 7 | version = 'develop' 8 | extra = {'debug': 1} 9 | 10 | 11 | class StagingHost(Staging, BackEnd, FrontEnd): 12 | ansible_host = 'master_hostname' 13 | version = 'master' 14 | extra_branches = ['foo', 'bar'] 15 | extra_objs = [ 16 | { 17 | 'prop1': 'value1', 18 | 'prop2': 'value2', 19 | }, 20 | { 21 | 'prop3': 'value3', 22 | 'prop4': 'value4', 23 | }, 24 | ] 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ci: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.10', '3.11.0-rc.2 - 3.11.0'] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install poetry 22 | run: pip install 'poetry==1.1.0b3' wheel 23 | - name: Install deps 24 | run: poetry install 25 | - name: Code health 26 | run: poetry run python -m pytest --mypy --flake8 --isort --black pyventory/ 27 | - name: Tests 28 | run: poetry run python -m pytest tests/ 29 | -------------------------------------------------------------------------------- /tests/test_asset.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyventory import Asset, errors 4 | 5 | 6 | def test_calculate_asset_class_attribute_value_on_call(): 7 | class TestAsset(Asset): 8 | foo = '{bar}' 9 | bar = 'bar' 10 | 11 | assert TestAsset.foo() == 'bar' 12 | 13 | 14 | def test_escaped_braces_do_not_act_as_a_value_template(): 15 | class TestAsset(Asset): 16 | foo = '{{bar}}' 17 | 18 | assert TestAsset.foo() == '{bar}' 19 | 20 | 21 | def test_use_raw_asset_class_attribute_value(): 22 | class TestAsset(Asset): 23 | foo = '{bar}-{baz}' 24 | bar = 'bar' 25 | 26 | assert TestAsset.foo == '{bar}-{baz}' 27 | 28 | 29 | def test_asset_class_attribute_value_calculation_is_strict(): 30 | class TestAsset(Asset): 31 | foo = '{bar}-{baz}' 32 | bar = 'bar' 33 | 34 | with pytest.raises(errors.ValueSubstitutionError): 35 | TestAsset.foo() 36 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: isort 5 | name: isort 6 | entry: poetry run isort 7 | language: system 8 | types: [python] 9 | - id: mypy 10 | name: mypy 11 | entry: poetry run mypy 12 | language: system 13 | types: [python] 14 | exclude: > 15 | (?x)^( 16 | docs/ | 17 | tests/ 18 | ) 19 | - id: black 20 | name: black 21 | entry: poetry run black 22 | language: system 23 | types: [python] 24 | - id: flake8 25 | name: flake8 26 | entry: poetry run flake8 27 | language: system 28 | types: [python] 29 | exclude: > 30 | (?x)^( 31 | tests/example/ 32 | ) 33 | - id: rstcheck 34 | name: rstcheck 35 | entry: poetry run rstcheck 36 | language: system 37 | types: [rst] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Serge Matveenko 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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0a5"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "pyventory" 7 | version = "3.3.2" 8 | description = "Ansible Inventory implementation that uses Python syntax" 9 | authors = ["Serge Matveenko "] 10 | license = "MIT" 11 | readme = "README.md" 12 | homepage = "https://github.com/lig/pyventory" 13 | repository = "https://github.com/lig/pyventory.git" 14 | documentation = "https://readthedocs.org/projects/pyventory/" 15 | keywords = ["devops", "ansible", "inventory", "terraform", "vars"] 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Intended Audience :: Developers", 19 | "Intended Audience :: System Administrators", 20 | "Topic :: System :: Systems Administration", 21 | ] 22 | 23 | [tool.poetry.dependencies] 24 | python = "^3.10" 25 | attrs = "^22.1.0" 26 | ordered-set = "^4.0.2" 27 | 28 | [tool.poetry.dev-dependencies] 29 | pytest = "^7.1.3" 30 | pytest-black = "^0.3.11" 31 | pytest-flake8 = "^1.1.1" 32 | pytest-isort = "^3.0.0" 33 | pytest-mypy = "^0.10.0" 34 | rstcheck = "^6.1.0" 35 | Sphinx = "^5.2.3" 36 | 37 | [tool.black] 38 | skip-string-normalization = true 39 | 40 | [tool.isort] 41 | profile = "black" 42 | lines_after_imports = 2 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI Status](https://github.com/lig/pyventory/workflows/CI/badge.svg)](https://github.com/lig/pyventory/actions) 2 | 3 | # Pyventory 4 | 5 | Ansible Inventory implementation that uses Python syntax 6 | 7 | ## Install 8 | 9 | ```shell 10 | pip3 install pyventory 11 | ``` 12 | 13 | ## Features 14 | 15 | * Modular inventory. 16 | * Assests inheritance using Python classes. 17 | * Support for multiple inheritance. 18 | * Support for mixins. 19 | * Support for vars templating using [Python string formatting](https://docs.python.org/3/library/string.html#format-specification-mini-language). 20 | * Python 3 (>=3.6) support. 21 | * Python 2 is not supported. 22 | 23 | ## Usage 24 | 25 | Create `hosts.py` and make it executable. 26 | 27 | A short example of the `hosts.py` contents: 28 | 29 | ```python 30 | #!/usr/bin/env python3 31 | from pyventory import Asset, ansible_inventory 32 | 33 | class All(Asset): 34 | run_tests = False 35 | use_redis = False 36 | redis_host = 'localhost' 37 | minify = False 38 | version = 'develop' 39 | 40 | class Staging(All): 41 | run_tests = True 42 | 43 | staging = Staging() 44 | 45 | ansible_inventory(locals()) 46 | ``` 47 | 48 | Consider a [more complex example](tests/example) which passes the following [json output](tests/example.json) to Ansible. 49 | 50 | Run Ansible playbook with the `-i hosts.py` key: 51 | 52 | ```shell 53 | ansible-playbook -i hosts.py site.yml 54 | ``` 55 | 56 | Notice that you need to have your inventory package in `PYTHONPATH`. 57 | -------------------------------------------------------------------------------- /tests/test_example.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shlex 4 | import subprocess 5 | 6 | import pytest 7 | 8 | 9 | @pytest.fixture(scope='session') 10 | def example_dir(tests_dir): 11 | return tests_dir / 'example' 12 | 13 | 14 | @pytest.fixture(scope='session') 15 | def anisble_inventory(example_dir): 16 | return open(example_dir / 'ansible.json', 'r') 17 | 18 | 19 | @pytest.fixture(scope='session') 20 | def terraform_config(example_dir): 21 | return open(example_dir / 'terraform.tf.json', 'r') 22 | 23 | 24 | def test_ansible_inventory(tests_dir, example_dir, anisble_inventory): 25 | project_dir = tests_dir.parent 26 | inventory_exe = example_dir / 'ansible_hosts.py' 27 | 28 | result = subprocess.run( 29 | shlex.split(str(inventory_exe)), 30 | stdout=subprocess.PIPE, 31 | check=True, 32 | env=dict(os.environ, PYTHONPATH='{}:{}'.format(project_dir, example_dir)), 33 | ).stdout 34 | 35 | assert json.loads(result.decode()) == json.load(anisble_inventory) 36 | 37 | 38 | def test_terraform_vars(tests_dir, example_dir, terraform_config): 39 | project_dir = tests_dir.parent 40 | inventory_exe = example_dir / 'terraform_vars.py' 41 | 42 | subprocess.run( 43 | shlex.split(str(inventory_exe)), 44 | check=True, 45 | env=dict(os.environ, PYTHONPATH='{}:{}'.format(project_dir, example_dir)), 46 | ) 47 | 48 | result_path = example_dir / 'terraform_result.tf.json' 49 | result = open(result_path, 'r') 50 | 51 | assert json.load(result) == json.load(terraform_config) 52 | 53 | result_path.unlink() 54 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | A New Feel for Ansible Inventory 2 | ================================ 3 | 4 | `Pyventory` is a tricky twist in thinking about the way to create and maintain Ansible 5 | inventory. It brings the power of OOP into play to defend from some common errors like 6 | undefined variables and to help keep the inventory in line with the DRY principle. 7 | 8 | 9 | Installation 10 | ============ 11 | 12 | It is usually enough to install `Pyventory` into the same Python environment as you have 13 | Ansible installed into. The command below should make it happen in most cases. 14 | 15 | .. code-block:: shell 16 | 17 | pip3 install pyventory 18 | 19 | 20 | If the above doesn't work for you, read :doc:`/guide/installation`. 21 | 22 | 23 | Contents 24 | ======== 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | 29 | tutorial/index 30 | guide/index 31 | how-tos/index 32 | 33 | 34 | Contributing 35 | ============ 36 | 37 | We welcome any help including but not limited to bug reports, feature requests, 38 | translations, documentation improvements, packaging. It could be anything you think 39 | could help you and others to use or to improve the project. 40 | 41 | Use `our GitHub page`_. Be sure to review the `contributing guidelines`_ and `code of conduct`_. 42 | 43 | 44 | Indices and tables 45 | ================== 46 | 47 | * :ref:`genindex` 48 | * :ref:`modindex` 49 | * :ref:`search` 50 | 51 | .. _`our GitHub page`: https://github.com/lig/pyventory 52 | .. _`code of conduct`: https://github.com/lig/pyventory/blob/main/CODE_OF_CONDUCT.md 53 | .. _`contributing guidelines`: https://github.com/lig/pyventory/blob/main/CONTRIBUTING.md 54 | -------------------------------------------------------------------------------- /pyventory/base.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from collections import abc 3 | 4 | import attr 5 | from ordered_set import OrderedSet 6 | 7 | from . import assets 8 | 9 | 10 | MaybeAsset_T = typing.Union[assets.Asset, typing.Any] 11 | 12 | 13 | @attr.s(auto_attribs=True, kw_only=True) 14 | class AssetData: 15 | vars: typing.Mapping[str, assets.AttrValueFinal_T] = attr.ib( 16 | default=attr.Factory(dict) 17 | ) 18 | children: typing.MutableSet[str] = attr.ib(default=attr.Factory(OrderedSet)) 19 | instances: typing.Set[str] = attr.ib(default=attr.Factory(OrderedSet)) 20 | 21 | 22 | class Inventory: 23 | def __init__(self, instances: typing.Mapping[str, MaybeAsset_T]): 24 | self.assets: typing.MutableMapping[str, AssetData] = {} 25 | self.instances: typing.Dict[ 26 | str, typing.Mapping[str, assets.AttrValueFinal_T] 27 | ] = {} 28 | 29 | for name, instance in sorted(instances.items()): 30 | self.add_instance(name, instance) 31 | 32 | def add_instance(self, name: str, instance: MaybeAsset_T) -> None: 33 | if isinstance(instance, abc.Iterable) and not isinstance(instance, str): 34 | for n, item in enumerate(instance, start=1): 35 | self.add_instance(name=f'{name}_{n}', instance=item) 36 | return 37 | 38 | if not isinstance(instance, assets.Asset): 39 | return 40 | 41 | self.instances[name] = instance._context(instance) 42 | self.add_asset(instance.__class__) 43 | self.assets[instance._name].instances.add(name) 44 | 45 | def add_asset(self, asset: typing.Type[assets.Asset]) -> None: 46 | if asset._name in self.assets: 47 | return 48 | 49 | for parent_asset in asset.__bases__: 50 | # skip mixins 51 | if not issubclass(parent_asset, assets.Asset): 52 | continue 53 | # skip Asset itself 54 | if parent_asset is assets.Asset: 55 | continue 56 | 57 | self.add_asset(parent_asset) 58 | self.assets[parent_asset._name].children.add(asset._name) 59 | 60 | self.assets[asset._name] = AssetData(vars=asset._vars(asset)) 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Pyventory 2 | 3 | ## How to… 4 | * [Propose a Feature](#propose-a-feature) 5 | * [File a Bug](#file-a-bug) 6 | * [Change the Code](#change-the-code) 7 | * [Change the Docs](#change-the-docs) 8 | * [Get Support](#get-support) 9 | 10 | ## Propose a Feature 11 | 12 | * Open an issue here: https://github.com/lig/pyventory/issues/new. 13 | * Describe a feature you are proposing in a free form. 14 | * Provide one or more real-life use cases. 15 | * If there are other tools that already has the feature describe the way it works there. 16 | 17 | ## File a Bug 18 | 19 | * Open an issue here: https://github.com/lig/pyventory/issues/new. 20 | * Provide the output of `ansible --version`. 21 | * Provide the Pyventory version you have. It could be usually obtained with something like `pip show pyventory`. 22 | * Provide the part of your inventory causing problems. Feel free to reduce it to a minimum required to reproduce the issue. 23 | * Describe steps needed to reproduce the issue. 24 | * Provide the output having error messages if applicable. 25 | 26 | ## Change the Code 27 | 28 | > Note: the main integration branch in Pyventory repo is `main`. 29 | 30 | * You need [`poetry`](https://python-poetry.org/docs/#installation) and [`pre-commit`](https://pre-commit.com/#install) to be installed in your local environment. 31 | * Fork the project on GiHub: https://github.com/lig/pyventory/fork. 32 | * Clone the project locally. 33 | * In the root dir of the local copy run: 34 | - `poetry install` 35 | - `pre-commit install` 36 | * Create a new branch (in case you'll need to make another change before this one is merged upstream). 37 | * Make changes to the code. 38 | * Ensure tests are passing (it's a good idea to add some): `poetry run pytest tests/`. 39 | * Push you changes. 40 | * Open a PR. 41 | * Be ready to address maintaners' comments after they review you code. 42 | * Respect the [Code of Conduct](CODE_OF_CONDUCT.md). 43 | 44 | ## Change the Docs 45 | 46 | Follow the steps in [Change the Code](#change-the-docs) except you won't need to run the tests. 47 | 48 | ## Get Support 49 | 50 | Whilst it's possible to use GitHub issues to ask a question the best way to get the support for Pyventory is to use the [`pyventory` tag on Stack Overflow](https://stackoverflow.com/questions/tagged/pyventory). 51 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Pyventory' 21 | copyright = '2020, Serge Matveenko' 22 | author = 'Serge Matveenko' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # List of patterns, relative to source directory, that match files and 36 | # directories to ignore when looking for source files. 37 | # This pattern also affects html_static_path and html_extra_path. 38 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 39 | 40 | 41 | # -- Options for HTML output ------------------------------------------------- 42 | 43 | # The theme to use for HTML and HTML Help pages. See the documentation for 44 | # a list of builtin themes. 45 | # 46 | html_theme = 'alabaster' 47 | 48 | # Add any paths that contain custom static files (such as style sheets) here, 49 | # relative to this directory. They are copied after the builtin static files, 50 | # so a file named "default.css" will overwrite the builtin "default.css". 51 | html_static_path = ['_static'] 52 | 53 | 54 | # https://github.com/readthedocs/readthedocs.org/issues/2569 55 | master_doc = 'index' 56 | 57 | 58 | # Alabaster theme options 59 | 60 | html_theme_options = { 61 | 'description': "A New Feel for Ansible Inventory", 62 | 'fixed_sidebar': True, 63 | 'show_relbar_bottom': True, 64 | 'github_banner': True, 65 | 'github_button': True, 66 | 'github_user': "lig", 67 | 'github_repo': "pyventory", 68 | 'github_type': 'star', 69 | 'github_count': 'true', 70 | } 71 | -------------------------------------------------------------------------------- /pyventory/export.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | import sys 4 | import typing 5 | from collections import abc 6 | 7 | import attr 8 | 9 | from . import base 10 | 11 | 12 | def pyventory_data(instances: typing.Mapping[str, base.MaybeAsset_T]) -> typing.Mapping: 13 | """Provides raw inventory data as Python `dict` containing Asset data in 14 | `assets` key and instances data in `instances` key. 15 | """ 16 | inventory = base.Inventory(instances) 17 | 18 | assets = {name: attr.asdict(asset) for name, asset in inventory.assets.items()} 19 | 20 | for asset in assets.values(): 21 | for attr_name in ( 22 | 'instances', 23 | 'vars', 24 | 'children', 25 | ): 26 | if not asset[attr_name]: 27 | del asset[attr_name] 28 | 29 | instances = inventory.instances.copy() 30 | 31 | return {'assets': assets, 'instances': instances} 32 | 33 | 34 | def ansible_inventory( 35 | hosts: typing.Mapping[str, base.MaybeAsset_T], 36 | out: typing.TextIO = sys.stdout, 37 | indent: typing.Optional[int] = None, 38 | ) -> None: 39 | """Dumps inventory in the Ansible's Dynamic Inventory JSON format to `out`.""" 40 | raw_data = pyventory_data(hosts) 41 | 42 | data = {} 43 | 44 | for key, value in raw_data['assets'].items(): 45 | if 'instances' in value: 46 | value['hosts'] = value.pop('instances') 47 | data[key] = value 48 | 49 | data['_meta'] = {'hostvars': raw_data['instances']} 50 | 51 | json.dump(data, out, indent=indent, default=list) 52 | 53 | 54 | def terraform_vars( 55 | instances: typing.Mapping[str, base.MaybeAsset_T], 56 | filename_base: str = 'pyventory', 57 | indent: typing.Optional[int] = None, 58 | ) -> None: 59 | """Dumps inventory in the Terraform's JSON format to `.tf` 60 | setting their values as defaults. 61 | """ 62 | tf_config_path = pathlib.Path(filename_base).with_suffix('.tf.json') 63 | 64 | raw_data = pyventory_data(instances) 65 | 66 | tf_config = {} 67 | 68 | for asset_name, asset_data in raw_data['instances'].items(): 69 | 70 | for name, value in asset_data.items(): 71 | 72 | var_name = f'{asset_name}__{name}' 73 | 74 | var_type = 'string' 75 | var_value = value 76 | 77 | if isinstance(value, str): 78 | pass 79 | elif isinstance(value, bool): 80 | var_value = str(value).lower() 81 | elif isinstance( 82 | value, 83 | ( 84 | int, 85 | float, 86 | ), 87 | ): 88 | var_value = str(value) 89 | elif isinstance(value, abc.Mapping): 90 | var_type = 'map' 91 | elif isinstance(value, abc.Iterable): 92 | if value and isinstance(next(iter(value)), abc.Mapping): 93 | var_type = 'map' 94 | else: 95 | var_type = 'list' 96 | 97 | tf_config[var_name] = { 98 | 'type': var_type, 99 | 'default': var_value, 100 | } 101 | 102 | tf_config = {'variable': tf_config} 103 | 104 | json.dump(tf_config, open(tf_config_path, 'w'), indent=indent) 105 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lig+pyventory@countzero.co. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /pyventory/assets.py: -------------------------------------------------------------------------------- 1 | import re 2 | import types 3 | import typing 4 | from collections import abc 5 | 6 | from pyventory import errors 7 | 8 | 9 | __all__ = ['Asset'] 10 | 11 | 12 | AssetObj_T = typing.Union['Asset', typing.Type['Asset']] 13 | AttrValueFinal_T = typing.Union[str, int, float, typing.Iterable, typing.Mapping] 14 | AttrValueDef_T = typing.Union[types.NotImplementedType, AttrValueFinal_T] 15 | AssetContext_T = typing.Dict[str, AttrValueDef_T] 16 | 17 | 18 | class SKIP_ATTR: 19 | pass 20 | 21 | 22 | class AssetAttr: 23 | _value: AttrValueDef_T 24 | _name: str 25 | 26 | def __init__(self, value: AttrValueDef_T) -> None: 27 | self._value = value 28 | 29 | def __get__( 30 | self, instance: typing.Optional['Asset'], owner: typing.Type['Asset'] 31 | ) -> AttrValueDef_T: 32 | if instance is not None: 33 | return self._value 34 | 35 | if not isinstance(self._value, (str, abc.Mapping, abc.Sequence)): 36 | return self._value 37 | 38 | def get_attr() -> AttrValueFinal_T: 39 | return typing.cast( 40 | AttrValueFinal_T, owner._get_attr(owner, self._name, strict=True) 41 | ) 42 | 43 | value_type = type(self._value) 44 | return type( 45 | value_type.__name__, (value_type,), {'__call__': staticmethod(get_attr)} 46 | )(self._value) 47 | 48 | def __set_name__(self, owner: typing.Type['Asset'], name: str) -> None: 49 | self._name = name 50 | 51 | 52 | class AssetMeta(type): 53 | def __new__( 54 | cls, 55 | name: str, 56 | bases: typing.Tuple[typing.Type, ...], 57 | namespace: typing.Dict[str, typing.Any], 58 | **kwds: typing.Any, 59 | ) -> 'AssetMeta': 60 | new_namespace = { 61 | '_name': f'{namespace["__module__"]}.{name}', 62 | } 63 | 64 | for key, value in namespace.items(): 65 | if not key.startswith('_'): 66 | value = AssetAttr(value) 67 | new_namespace[key] = value 68 | 69 | return typing.cast( 70 | 'AssetMeta', super().__new__(cls, name, bases, new_namespace) 71 | ) 72 | 73 | 74 | class Asset(metaclass=AssetMeta): 75 | _name: typing.ClassVar[str] 76 | 77 | _string_format_regex = re.compile(r'(? 'Asset': 80 | self = super().__new__(cls) 81 | self.__dict__.update(kwargs) 82 | self.__dict__.update(self._vars(self, strict=True)) 83 | return self 84 | 85 | @staticmethod 86 | def _attrs(obj: AssetObj_T) -> typing.Iterable[str]: 87 | return [name for name in dir(obj) if not name.startswith('_')] 88 | 89 | @staticmethod 90 | def _context(obj: AssetObj_T) -> AssetContext_T: 91 | return {name: getattr(obj, name) for name in obj._attrs(obj)} 92 | 93 | @staticmethod 94 | def _vars( 95 | obj: AssetObj_T, strict: bool = False 96 | ) -> typing.Mapping[str, AttrValueFinal_T]: 97 | return { 98 | name: typing.cast(AttrValueFinal_T, value) 99 | for name, value in ( 100 | (name, obj._get_attr(obj, name, strict=strict)) 101 | for name in obj._attrs(obj) 102 | ) 103 | if value is not SKIP_ATTR 104 | } 105 | 106 | @staticmethod 107 | def _get_attr( 108 | obj: AssetObj_T, name: str, strict: bool = False 109 | ) -> typing.Union[AttrValueFinal_T, typing.Type[SKIP_ATTR]]: 110 | try: 111 | context = obj._context(obj).copy() 112 | return obj._format_value(obj, context, context[name], name) 113 | except NotImplementedError: 114 | if strict: 115 | raise errors.PropertyIsNotImplementedError( 116 | f'Var "{name}" is not implemented in "{obj._name}" asset' 117 | ) 118 | else: 119 | return SKIP_ATTR 120 | except KeyError as e: 121 | if strict: 122 | raise errors.ValueSubstitutionError( 123 | f'Attribute "{e.args[0]}" must be available for' 124 | f' "{obj._name}" asset instance' 125 | ) 126 | else: 127 | return SKIP_ATTR 128 | except errors.ValueSubstitutionInfiniteLoopError: 129 | raise errors.ValueSubstitutionInfiniteLoopError( 130 | f'Attribute "{name}" has an infinite string substitution' 131 | f' loop in "{obj._name}" asset instance' 132 | ) 133 | 134 | @staticmethod 135 | def _format_value( 136 | obj: AssetObj_T, context: AssetContext_T, value: AttrValueDef_T, start_key: str 137 | ) -> AttrValueFinal_T: 138 | if value is NotImplemented: 139 | raise NotImplementedError 140 | if isinstance(value, str): 141 | for key in obj._string_format_regex.findall(value): 142 | if key == start_key: 143 | raise errors.ValueSubstitutionInfiniteLoopError 144 | context[key] = obj._format_value(obj, context, context[key], start_key) 145 | return value.format(**context) 146 | if isinstance(value, abc.Mapping): 147 | return { 148 | k: obj._format_value(obj, context, v, start_key) 149 | for k, v in value.items() 150 | } 151 | if isinstance(value, abc.Iterable): 152 | return [obj._format_value(obj, context, v, start_key) for v in value] 153 | return value 154 | -------------------------------------------------------------------------------- /tests/example/terraform.tf.json: -------------------------------------------------------------------------------- 1 | { 2 | "variable": { 3 | "develop__ansible_host": { 4 | "type": "string", 5 | "default": "develop_hostname" 6 | }, 7 | "develop__extra": { 8 | "type": "map", 9 | "default": { 10 | "debug": 1 11 | } 12 | }, 13 | "develop__minify": { 14 | "type": "string", 15 | "default": "false" 16 | }, 17 | "develop__redis_host": { 18 | "type": "string", 19 | "default": "localhost" 20 | }, 21 | "develop__run_tests": { 22 | "type": "string", 23 | "default": "true" 24 | }, 25 | "develop__use_redis": { 26 | "type": "string", 27 | "default": "true" 28 | }, 29 | "develop__version": { 30 | "type": "string", 31 | "default": "develop" 32 | }, 33 | "develop_sidebranch__ansible_host": { 34 | "type": "string", 35 | "default": "sidebranch_hostname" 36 | }, 37 | "develop_sidebranch__extra": { 38 | "type": "map", 39 | "default": { 40 | "debug": 1 41 | } 42 | }, 43 | "develop_sidebranch__minify": { 44 | "type": "string", 45 | "default": "false" 46 | }, 47 | "develop_sidebranch__redis_host": { 48 | "type": "string", 49 | "default": "localhost" 50 | }, 51 | "develop_sidebranch__run_tests": { 52 | "type": "string", 53 | "default": "true" 54 | }, 55 | "develop_sidebranch__use_redis": { 56 | "type": "string", 57 | "default": "true" 58 | }, 59 | "develop_sidebranch__version": { 60 | "type": "string", 61 | "default": "sidebranch_name" 62 | }, 63 | "prod_backend1__ansible_hostname": { 64 | "type": "string", 65 | "default": "app001.prod.dom" 66 | }, 67 | "prod_backend1__minify": { 68 | "type": "string", 69 | "default": "true" 70 | }, 71 | "prod_backend1__num": { 72 | "type": "string", 73 | "default": "1" 74 | }, 75 | "prod_backend1__redis_host": { 76 | "type": "string", 77 | "default": "prod_redis_hostname" 78 | }, 79 | "prod_backend1__run_tests": { 80 | "type": "string", 81 | "default": "false" 82 | }, 83 | "prod_backend1__use_redis": { 84 | "type": "string", 85 | "default": "true" 86 | }, 87 | "prod_backend1__version": { 88 | "type": "string", 89 | "default": "master" 90 | }, 91 | "prod_backend2__ansible_hostname": { 92 | "type": "string", 93 | "default": "app002.prod.dom" 94 | }, 95 | "prod_backend2__minify": { 96 | "type": "string", 97 | "default": "true" 98 | }, 99 | "prod_backend2__num": { 100 | "type": "string", 101 | "default": "2" 102 | }, 103 | "prod_backend2__redis_host": { 104 | "type": "string", 105 | "default": "prod_redis_hostname" 106 | }, 107 | "prod_backend2__run_tests": { 108 | "type": "string", 109 | "default": "false" 110 | }, 111 | "prod_backend2__use_redis": { 112 | "type": "string", 113 | "default": "true" 114 | }, 115 | "prod_backend2__version": { 116 | "type": "string", 117 | "default": "master" 118 | }, 119 | "prod_frontend1__ansible_hostname": { 120 | "type": "string", 121 | "default": "www001.prod.dom" 122 | }, 123 | "prod_frontend1__minify": { 124 | "type": "string", 125 | "default": "true" 126 | }, 127 | "prod_frontend1__num": { 128 | "type": "string", 129 | "default": "1" 130 | }, 131 | "prod_frontend1__redis_host": { 132 | "type": "string", 133 | "default": "localhost" 134 | }, 135 | "prod_frontend1__run_tests": { 136 | "type": "string", 137 | "default": "false" 138 | }, 139 | "prod_frontend1__use_redis": { 140 | "type": "string", 141 | "default": "true" 142 | }, 143 | "prod_frontend1__version": { 144 | "type": "string", 145 | "default": "master" 146 | }, 147 | "prod_frontend2__ansible_hostname": { 148 | "type": "string", 149 | "default": "www002.prod.dom" 150 | }, 151 | "prod_frontend2__minify": { 152 | "type": "string", 153 | "default": "true" 154 | }, 155 | "prod_frontend2__num": { 156 | "type": "string", 157 | "default": "2" 158 | }, 159 | "prod_frontend2__redis_host": { 160 | "type": "string", 161 | "default": "localhost" 162 | }, 163 | "prod_frontend2__run_tests": { 164 | "type": "string", 165 | "default": "false" 166 | }, 167 | "prod_frontend2__use_redis": { 168 | "type": "string", 169 | "default": "true" 170 | }, 171 | "prod_frontend2__version": { 172 | "type": "string", 173 | "default": "master" 174 | }, 175 | "staging__ansible_host": { 176 | "type": "string", 177 | "default": "master_hostname" 178 | }, 179 | "staging__extra_branches": { 180 | "type": "list", 181 | "default": [ 182 | "foo", 183 | "bar" 184 | ] 185 | }, 186 | "staging__extra_objs": { 187 | "type": "map", 188 | "default": [ 189 | { 190 | "prop1": "value1", 191 | "prop2": "value2" 192 | }, 193 | { 194 | "prop3": "value3", 195 | "prop4": "value4" 196 | } 197 | ] 198 | }, 199 | "staging__minify": { 200 | "type": "string", 201 | "default": "false" 202 | }, 203 | "staging__redis_host": { 204 | "type": "string", 205 | "default": "localhost" 206 | }, 207 | "staging__run_tests": { 208 | "type": "string", 209 | "default": "true" 210 | }, 211 | "staging__use_redis": { 212 | "type": "string", 213 | "default": "true" 214 | }, 215 | "staging__version": { 216 | "type": "string", 217 | "default": "master" 218 | } 219 | } 220 | } -------------------------------------------------------------------------------- /tests/example/ansible.json: -------------------------------------------------------------------------------- 1 | { 2 | "inventory.base.All": { 3 | "vars": { 4 | "minify": false, 5 | "redis_host": "localhost", 6 | "run_tests": false, 7 | "use_redis": false, 8 | "version": "develop" 9 | }, 10 | "children": [ 11 | "inventory.env.Staging", 12 | "inventory.project.BackEnd", 13 | "inventory.project.FrontEnd", 14 | "inventory.env.Production" 15 | ] 16 | }, 17 | "inventory.env.Staging": { 18 | "vars": { 19 | "minify": false, 20 | "redis_host": "localhost", 21 | "run_tests": true, 22 | "use_redis": false, 23 | "version": "develop" 24 | }, 25 | "children": [ 26 | "inventory.stages.DevelopHost", 27 | "inventory.stages.StagingHost" 28 | ] 29 | }, 30 | "inventory.project.BackEnd": { 31 | "vars": { 32 | "minify": false, 33 | "redis_host": "localhost", 34 | "run_tests": false, 35 | "use_redis": true, 36 | "version": "develop" 37 | }, 38 | "children": [ 39 | "inventory.stages.DevelopHost", 40 | "inventory.prod.ProdBackEnd", 41 | "inventory.stages.StagingHost" 42 | ] 43 | }, 44 | "inventory.project.FrontEnd": { 45 | "vars": { 46 | "minify": false, 47 | "redis_host": "localhost", 48 | "run_tests": false, 49 | "use_redis": false, 50 | "version": "develop" 51 | }, 52 | "children": [ 53 | "inventory.stages.DevelopHost", 54 | "inventory.prod.ProdFrontEnd", 55 | "inventory.stages.StagingHost" 56 | ] 57 | }, 58 | "inventory.stages.DevelopHost": { 59 | "vars": { 60 | "ansible_host": "develop_hostname", 61 | "extra": { 62 | "debug": 1 63 | }, 64 | "minify": false, 65 | "redis_host": "localhost", 66 | "run_tests": true, 67 | "use_redis": true, 68 | "version": "develop" 69 | }, 70 | "hosts": [ 71 | "develop", 72 | "develop_sidebranch" 73 | ] 74 | }, 75 | "inventory.env.Production": { 76 | "vars": { 77 | "minify": true, 78 | "redis_host": "localhost", 79 | "run_tests": false, 80 | "use_redis": true, 81 | "version": "master" 82 | }, 83 | "children": [ 84 | "inventory.prod.ProdBackEnd", 85 | "inventory.prod.ProdFrontEnd" 86 | ] 87 | }, 88 | "inventory.prod.ProdBackEnd": { 89 | "vars": { 90 | "minify": true, 91 | "redis_host": "prod_redis_hostname", 92 | "run_tests": false, 93 | "use_redis": true, 94 | "version": "master" 95 | }, 96 | "hosts": [ 97 | "prod_backend1", 98 | "prod_backend2" 99 | ] 100 | }, 101 | "inventory.prod.ProdFrontEnd": { 102 | "vars": { 103 | "minify": true, 104 | "redis_host": "localhost", 105 | "run_tests": false, 106 | "use_redis": true, 107 | "version": "master" 108 | }, 109 | "hosts": [ 110 | "prod_frontend1", 111 | "prod_frontend2" 112 | ] 113 | }, 114 | "inventory.stages.StagingHost": { 115 | "vars": { 116 | "ansible_host": "master_hostname", 117 | "extra_branches": [ 118 | "foo", 119 | "bar" 120 | ], 121 | "extra_objs": [ 122 | { 123 | "prop1": "value1", 124 | "prop2": "value2" 125 | }, 126 | { 127 | "prop3": "value3", 128 | "prop4": "value4" 129 | } 130 | ], 131 | "minify": false, 132 | "redis_host": "localhost", 133 | "run_tests": true, 134 | "use_redis": true, 135 | "version": "master" 136 | }, 137 | "hosts": [ 138 | "staging" 139 | ] 140 | }, 141 | "_meta": { 142 | "hostvars": { 143 | "develop": { 144 | "ansible_host": "develop_hostname", 145 | "extra": { 146 | "debug": 1 147 | }, 148 | "minify": false, 149 | "redis_host": "localhost", 150 | "run_tests": true, 151 | "use_redis": true, 152 | "version": "develop" 153 | }, 154 | "develop_sidebranch": { 155 | "ansible_host": "sidebranch_hostname", 156 | "extra": { 157 | "debug": 1 158 | }, 159 | "minify": false, 160 | "redis_host": "localhost", 161 | "run_tests": true, 162 | "use_redis": true, 163 | "version": "sidebranch_name" 164 | }, 165 | "prod_backend1": { 166 | "ansible_hostname": "app001.prod.dom", 167 | "minify": true, 168 | "num": 1, 169 | "redis_host": "prod_redis_hostname", 170 | "run_tests": false, 171 | "use_redis": true, 172 | "version": "master" 173 | }, 174 | "prod_backend2": { 175 | "ansible_hostname": "app002.prod.dom", 176 | "minify": true, 177 | "num": 2, 178 | "redis_host": "prod_redis_hostname", 179 | "run_tests": false, 180 | "use_redis": true, 181 | "version": "master" 182 | }, 183 | "prod_frontend1": { 184 | "ansible_hostname": "www001.prod.dom", 185 | "minify": true, 186 | "num": 1, 187 | "redis_host": "localhost", 188 | "run_tests": false, 189 | "use_redis": true, 190 | "version": "master" 191 | }, 192 | "prod_frontend2": { 193 | "ansible_hostname": "www002.prod.dom", 194 | "minify": true, 195 | "num": 2, 196 | "redis_host": "localhost", 197 | "run_tests": false, 198 | "use_redis": true, 199 | "version": "master" 200 | }, 201 | "staging": { 202 | "ansible_host": "master_hostname", 203 | "extra_branches": [ 204 | "foo", 205 | "bar" 206 | ], 207 | "extra_objs": [ 208 | { 209 | "prop1": "value1", 210 | "prop2": "value2" 211 | }, 212 | { 213 | "prop3": "value3", 214 | "prop4": "value4" 215 | } 216 | ], 217 | "minify": false, 218 | "redis_host": "localhost", 219 | "run_tests": true, 220 | "use_redis": true, 221 | "version": "master" 222 | } 223 | } 224 | } 225 | } -------------------------------------------------------------------------------- /tests/test_inventory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyventory import Asset, errors, pyventory_data 4 | 5 | 6 | def test_allow_mixins_for_inventory_items(): 7 | class BaseTestAsset(Asset): 8 | pass 9 | 10 | class TestMixin: 11 | pass 12 | 13 | class TestAsset(TestMixin, BaseTestAsset): 14 | pass 15 | 16 | test_asset = TestAsset() 17 | 18 | result = pyventory_data(locals()) 19 | 20 | assert result == { 21 | 'assets': { 22 | "test_inventory.BaseTestAsset": { 23 | "children": ["test_inventory.TestAsset"], 24 | }, 25 | "test_inventory.TestAsset": { 26 | "instances": [ 27 | "test_asset", 28 | ], 29 | }, 30 | }, 31 | "instances": {"test_asset": {}}, 32 | } 33 | 34 | 35 | def test_allow_host_specific_vars(): 36 | class TestAsset(Asset): 37 | pass 38 | 39 | test_asset = TestAsset(foo='bar') 40 | 41 | result = pyventory_data(locals()) 42 | 43 | assert result == { 44 | 'assets': { 45 | "test_inventory.TestAsset": { 46 | "instances": [ 47 | "test_asset", 48 | ], 49 | }, 50 | }, 51 | "instances": { 52 | "test_asset": {"foo": "bar"}, 53 | }, 54 | } 55 | 56 | 57 | def test_allow_format_strings_as_values(): 58 | class TestAsset(Asset): 59 | foo = 'test_{bar}' 60 | 61 | test_asset = TestAsset(bar='ham') 62 | 63 | result = pyventory_data(locals()) 64 | 65 | assert result == { 66 | 'assets': { 67 | "test_inventory.TestAsset": {"instances": ["test_asset"]}, 68 | }, 69 | "instances": {"test_asset": {"bar": "ham", "foo": "test_ham"}}, 70 | } 71 | 72 | 73 | def test_allow_mapping_of_format_strings_as_values(): 74 | class TestAsset(Asset): 75 | foo = dict( 76 | baz='test_{bar}', 77 | ) 78 | 79 | test_asset = TestAsset(bar='ham') 80 | 81 | result = pyventory_data(locals()) 82 | 83 | assert result == { 84 | 'assets': { 85 | "test_inventory.TestAsset": {"instances": ["test_asset"]}, 86 | }, 87 | "instances": {"test_asset": {"bar": "ham", "foo": {"baz": "test_ham"}}}, 88 | } 89 | 90 | 91 | def test_allow_sequence_of_format_strings_as_values(): 92 | class TestAsset(Asset): 93 | foo = ['baz', 'test_{bar}'] 94 | 95 | test_asset = TestAsset(bar='ham') 96 | 97 | result = pyventory_data(locals()) 98 | 99 | assert result == { 100 | 'assets': { 101 | "test_inventory.TestAsset": {"instances": ["test_asset"]}, 102 | }, 103 | "instances": {"test_asset": {"bar": "ham", "foo": ["baz", "test_ham"]}}, 104 | } 105 | 106 | 107 | def test_strings_formatting_do_not_conflict_with_numbers(): 108 | class TestAsset(Asset): 109 | foo = 42 110 | 111 | test_asset = TestAsset(bar='ham') 112 | 113 | result = pyventory_data(locals()) 114 | 115 | assert result == { 116 | 'assets': { 117 | "test_inventory.TestAsset": { 118 | "vars": {"foo": 42}, 119 | "instances": ["test_asset"], 120 | }, 121 | }, 122 | "instances": {"test_asset": {"bar": "ham", "foo": 42}}, 123 | } 124 | 125 | 126 | def test_require_arguments_for_format_strings(): 127 | class TestAsset(Asset): 128 | foo = '{bar}' 129 | 130 | with pytest.raises(errors.ValueSubstitutionError): 131 | TestAsset() 132 | 133 | 134 | def test_inheritance_with_format(): 135 | class ParentAsset(Asset): 136 | foo = '{bar}' 137 | 138 | class ChildAsset(ParentAsset): 139 | pass 140 | 141 | child_asset = ChildAsset(bar='ham') 142 | 143 | result = pyventory_data(locals()) 144 | 145 | assert result == { 146 | 'assets': { 147 | "test_inventory.ParentAsset": {"children": ["test_inventory.ChildAsset"]}, 148 | "test_inventory.ChildAsset": {"instances": ["child_asset"]}, 149 | }, 150 | "instances": {"child_asset": {"bar": "ham", "foo": "ham"}}, 151 | } 152 | 153 | 154 | def test_deep_multiple_inheritance_propagation(): 155 | class Level1Asset1(Asset): 156 | foo = 'Level1Asset1 foo value' 157 | 158 | class Level1Asset2(Asset): 159 | foo = 'Level1Asset2 foo value' 160 | bar = 'Level1Asset2 bar value' 161 | 162 | class Level2Asset3(Level1Asset1, Level1Asset2): 163 | pass 164 | 165 | class Level3Asset4(Level2Asset3): 166 | baz = 'Level3Asset4 baz value' 167 | 168 | level3_asset4 = Level3Asset4() 169 | 170 | result = pyventory_data(locals()) 171 | 172 | assert result == { 173 | 'assets': { 174 | "test_inventory.Level1Asset1": { 175 | "vars": {"foo": "Level1Asset1 foo value"}, 176 | "children": ["test_inventory.Level2Asset3"], 177 | }, 178 | "test_inventory.Level1Asset2": { 179 | "vars": { 180 | "bar": "Level1Asset2 bar value", 181 | "foo": "Level1Asset2 foo value", 182 | }, 183 | "children": ["test_inventory.Level2Asset3"], 184 | }, 185 | "test_inventory.Level2Asset3": { 186 | "vars": { 187 | "bar": "Level1Asset2 bar value", 188 | "foo": "Level1Asset1 foo value", 189 | }, 190 | "children": ["test_inventory.Level3Asset4"], 191 | }, 192 | "test_inventory.Level3Asset4": { 193 | "vars": { 194 | "bar": "Level1Asset2 bar value", 195 | "baz": "Level3Asset4 baz value", 196 | "foo": "Level1Asset1 foo value", 197 | }, 198 | "instances": ["level3_asset4"], 199 | }, 200 | }, 201 | "instances": { 202 | "level3_asset4": { 203 | "bar": "Level1Asset2 bar value", 204 | "baz": "Level3Asset4 baz value", 205 | "foo": "Level1Asset1 foo value", 206 | } 207 | }, 208 | } 209 | 210 | 211 | def test_skip_non_asset_locals(): 212 | class TestAsset(Asset): 213 | pass 214 | 215 | class TestObject: 216 | pass 217 | 218 | test_asset = TestAsset() 219 | test_object = TestObject() 220 | 221 | result = pyventory_data(locals()) 222 | 223 | assert result == { 224 | 'assets': { 225 | "test_inventory.TestAsset": {"instances": ["test_asset"]}, 226 | }, 227 | "instances": {"test_asset": {}}, 228 | } 229 | 230 | 231 | def test_multiple_children(): 232 | class BaseTestAsset(Asset): 233 | pass 234 | 235 | class TestAsset1(BaseTestAsset): 236 | pass 237 | 238 | class TestAsset2(BaseTestAsset): 239 | pass 240 | 241 | test_asset1 = TestAsset1() 242 | test_asset2 = TestAsset2() 243 | 244 | result = pyventory_data(locals()) 245 | 246 | assert result == { 247 | 'assets': { 248 | "test_inventory.BaseTestAsset": { 249 | "children": ["test_inventory.TestAsset1", "test_inventory.TestAsset2"] 250 | }, 251 | "test_inventory.TestAsset1": {"instances": ["test_asset1"]}, 252 | "test_inventory.TestAsset2": {"instances": ["test_asset2"]}, 253 | }, 254 | "instances": {"test_asset1": {}, "test_asset2": {}}, 255 | } 256 | 257 | 258 | def test_allow_notimplemented_value(): 259 | class BaseTestAsset(Asset): 260 | foo = NotImplemented 261 | 262 | class TestAsset(BaseTestAsset): 263 | foo = 'bar' 264 | 265 | test_asset = TestAsset() 266 | 267 | result = pyventory_data(locals()) 268 | 269 | assert result == { 270 | 'assets': { 271 | "test_inventory.BaseTestAsset": {"children": ["test_inventory.TestAsset"]}, 272 | "test_inventory.TestAsset": { 273 | "vars": {"foo": "bar"}, 274 | "instances": ["test_asset"], 275 | }, 276 | }, 277 | "instances": {"test_asset": {"foo": "bar"}}, 278 | } 279 | 280 | 281 | def test_raise_notimplemented_value_in_final_asset(): 282 | class BaseTestAsset(Asset): 283 | foo = NotImplemented 284 | 285 | class TestAsset(BaseTestAsset): 286 | pass 287 | 288 | with pytest.raises(errors.PropertyIsNotImplementedError): 289 | TestAsset() 290 | 291 | 292 | def test_string_format_does_not_miss_values(): 293 | class BaseTestAsset(Asset): 294 | baz = 'baz-value' 295 | 296 | class TestAsset1(BaseTestAsset): 297 | bar = '{baz}' 298 | foo = '{bar}' 299 | 300 | class TestAsset2(BaseTestAsset): 301 | bar = '{foo}' 302 | foo = '{baz}' 303 | 304 | test_asset_1 = TestAsset1() 305 | test_asset_2 = TestAsset2() 306 | 307 | result = pyventory_data(locals()) 308 | 309 | assert result == { 310 | 'assets': { 311 | "test_inventory.BaseTestAsset": { 312 | "vars": {"baz": "baz-value"}, 313 | "children": ["test_inventory.TestAsset1", "test_inventory.TestAsset2"], 314 | }, 315 | "test_inventory.TestAsset1": { 316 | "vars": {"bar": "baz-value", "baz": "baz-value", "foo": "baz-value"}, 317 | "instances": ["test_asset_1"], 318 | }, 319 | "test_inventory.TestAsset2": { 320 | "vars": {"bar": "baz-value", "baz": "baz-value", "foo": "baz-value"}, 321 | "instances": ["test_asset_2"], 322 | }, 323 | }, 324 | "instances": { 325 | "test_asset_1": { 326 | "bar": "baz-value", 327 | "baz": "baz-value", 328 | "foo": "baz-value", 329 | }, 330 | "test_asset_2": { 331 | "bar": "baz-value", 332 | "baz": "baz-value", 333 | "foo": "baz-value", 334 | }, 335 | }, 336 | } 337 | 338 | 339 | def test_string_format_detects_infinite_loop(): 340 | class TestAsset(Asset): 341 | bar = '{foo}' 342 | foo = '{bar}' 343 | 344 | with pytest.raises(errors.ValueSubstitutionInfiniteLoopError): 345 | TestAsset() 346 | 347 | 348 | def test_lists_of_instances(): 349 | class BaseTestAsset(Asset): 350 | pass 351 | 352 | class TestAsset1(BaseTestAsset): 353 | foo = NotImplemented 354 | 355 | class TestAsset2(BaseTestAsset): 356 | bar = NotImplemented 357 | 358 | test_assets1 = [TestAsset1(foo=x) for x in range(2)] 359 | test_assets2 = [TestAsset2(bar=y) for y in range(2, 3)] 360 | 361 | result = pyventory_data(locals()) 362 | 363 | assert result == { 364 | "assets": { 365 | "test_inventory.BaseTestAsset": { 366 | "children": ["test_inventory.TestAsset1", "test_inventory.TestAsset2"] 367 | }, 368 | "test_inventory.TestAsset1": { 369 | "instances": ["test_assets1_1", "test_assets1_2"] 370 | }, 371 | "test_inventory.TestAsset2": {"instances": ["test_assets2_1"]}, 372 | }, 373 | "instances": { 374 | "test_assets1_1": {"foo": 0}, 375 | "test_assets1_2": {"foo": 1}, 376 | "test_assets2_1": {"bar": 2}, 377 | }, 378 | } 379 | 380 | 381 | def test_escaped_braces(): 382 | class TestAsset(Asset): 383 | foo = '{{bar}}' 384 | 385 | test_asset1 = TestAsset() 386 | test_asset2 = TestAsset(foo='{{ham}}') 387 | 388 | result = pyventory_data(locals()) 389 | 390 | assert result == { 391 | "assets": { 392 | "test_inventory.TestAsset": { 393 | 'vars': {"foo": "{bar}"}, 394 | 'instances': ["test_asset1", "test_asset2"], 395 | }, 396 | }, 397 | "instances": { 398 | "test_asset1": {"foo": "{bar}"}, 399 | "test_asset2": {"foo": "{ham}"}, 400 | }, 401 | } 402 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alabaster" 3 | version = "0.7.12" 4 | description = "A configurable sidebar-enabled Sphinx theme" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "22.1.0" 12 | description = "Classes Without Boilerplate" 13 | category = "main" 14 | optional = false 15 | python-versions = ">=3.5" 16 | 17 | [package.extras] 18 | dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] 19 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 20 | tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] 21 | tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] 22 | 23 | [[package]] 24 | name = "Babel" 25 | version = "2.10.3" 26 | description = "Internationalization utilities" 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=3.6" 30 | 31 | [package.dependencies] 32 | pytz = ">=2015.7" 33 | 34 | [[package]] 35 | name = "black" 36 | version = "22.10.0" 37 | description = "The uncompromising code formatter." 38 | category = "dev" 39 | optional = false 40 | python-versions = ">=3.7" 41 | 42 | [package.dependencies] 43 | click = ">=8.0.0" 44 | mypy-extensions = ">=0.4.3" 45 | pathspec = ">=0.9.0" 46 | platformdirs = ">=2" 47 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 48 | 49 | [package.extras] 50 | colorama = ["colorama (>=0.4.3)"] 51 | d = ["aiohttp (>=3.7.4)"] 52 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 53 | uvloop = ["uvloop (>=0.15.2)"] 54 | 55 | [[package]] 56 | name = "certifi" 57 | version = "2022.9.24" 58 | description = "Python package for providing Mozilla's CA Bundle." 59 | category = "dev" 60 | optional = false 61 | python-versions = ">=3.6" 62 | 63 | [[package]] 64 | name = "charset-normalizer" 65 | version = "2.1.1" 66 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 67 | category = "dev" 68 | optional = false 69 | python-versions = ">=3.6.0" 70 | 71 | [package.extras] 72 | unicode_backport = ["unicodedata2"] 73 | 74 | [[package]] 75 | name = "click" 76 | version = "8.1.3" 77 | description = "Composable command line interface toolkit" 78 | category = "dev" 79 | optional = false 80 | python-versions = ">=3.7" 81 | 82 | [package.dependencies] 83 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 84 | 85 | [[package]] 86 | name = "colorama" 87 | version = "0.4.5" 88 | description = "Cross-platform colored terminal text." 89 | category = "dev" 90 | optional = false 91 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 92 | 93 | [[package]] 94 | name = "commonmark" 95 | version = "0.9.1" 96 | description = "Python parser for the CommonMark Markdown spec" 97 | category = "dev" 98 | optional = false 99 | python-versions = "*" 100 | 101 | [package.extras] 102 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 103 | 104 | [[package]] 105 | name = "docutils" 106 | version = "0.18.1" 107 | description = "Docutils -- Python Documentation Utilities" 108 | category = "dev" 109 | optional = false 110 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 111 | 112 | [[package]] 113 | name = "filelock" 114 | version = "3.8.0" 115 | description = "A platform independent file lock." 116 | category = "dev" 117 | optional = false 118 | python-versions = ">=3.7" 119 | 120 | [package.extras] 121 | docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] 122 | testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] 123 | 124 | [[package]] 125 | name = "flake8" 126 | version = "5.0.4" 127 | description = "the modular source code checker: pep8 pyflakes and co" 128 | category = "dev" 129 | optional = false 130 | python-versions = ">=3.6.1" 131 | 132 | [package.dependencies] 133 | mccabe = ">=0.7.0,<0.8.0" 134 | pycodestyle = ">=2.9.0,<2.10.0" 135 | pyflakes = ">=2.5.0,<2.6.0" 136 | 137 | [[package]] 138 | name = "idna" 139 | version = "3.4" 140 | description = "Internationalized Domain Names in Applications (IDNA)" 141 | category = "dev" 142 | optional = false 143 | python-versions = ">=3.5" 144 | 145 | [[package]] 146 | name = "imagesize" 147 | version = "1.4.1" 148 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 149 | category = "dev" 150 | optional = false 151 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 152 | 153 | [[package]] 154 | name = "iniconfig" 155 | version = "1.1.1" 156 | description = "iniconfig: brain-dead simple config-ini parsing" 157 | category = "dev" 158 | optional = false 159 | python-versions = "*" 160 | 161 | [[package]] 162 | name = "isort" 163 | version = "5.10.1" 164 | description = "A Python utility / library to sort Python imports." 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=3.6.1,<4.0" 168 | 169 | [package.extras] 170 | colors = ["colorama (>=0.4.3,<0.5.0)"] 171 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 172 | plugins = ["setuptools"] 173 | requirements_deprecated_finder = ["pip-api", "pipreqs"] 174 | 175 | [[package]] 176 | name = "Jinja2" 177 | version = "3.1.2" 178 | description = "A very fast and expressive template engine." 179 | category = "dev" 180 | optional = false 181 | python-versions = ">=3.7" 182 | 183 | [package.dependencies] 184 | MarkupSafe = ">=2.0" 185 | 186 | [package.extras] 187 | i18n = ["Babel (>=2.7)"] 188 | 189 | [[package]] 190 | name = "MarkupSafe" 191 | version = "2.1.1" 192 | description = "Safely add untrusted strings to HTML/XML markup." 193 | category = "dev" 194 | optional = false 195 | python-versions = ">=3.7" 196 | 197 | [[package]] 198 | name = "mccabe" 199 | version = "0.7.0" 200 | description = "McCabe checker, plugin for flake8" 201 | category = "dev" 202 | optional = false 203 | python-versions = ">=3.6" 204 | 205 | [[package]] 206 | name = "mypy" 207 | version = "0.982" 208 | description = "Optional static typing for Python" 209 | category = "dev" 210 | optional = false 211 | python-versions = ">=3.7" 212 | 213 | [package.dependencies] 214 | mypy-extensions = ">=0.4.3" 215 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 216 | typing-extensions = ">=3.10" 217 | 218 | [package.extras] 219 | dmypy = ["psutil (>=4.0)"] 220 | python2 = ["typed-ast (>=1.4.0,<2)"] 221 | reports = ["lxml"] 222 | 223 | [[package]] 224 | name = "mypy-extensions" 225 | version = "0.4.3" 226 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 227 | category = "dev" 228 | optional = false 229 | python-versions = "*" 230 | 231 | [[package]] 232 | name = "ordered-set" 233 | version = "4.1.0" 234 | description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" 235 | category = "main" 236 | optional = false 237 | python-versions = ">=3.7" 238 | 239 | [package.extras] 240 | dev = ["black", "mypy", "pytest"] 241 | 242 | [[package]] 243 | name = "packaging" 244 | version = "21.3" 245 | description = "Core utilities for Python packages" 246 | category = "dev" 247 | optional = false 248 | python-versions = ">=3.6" 249 | 250 | [package.dependencies] 251 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 252 | 253 | [[package]] 254 | name = "pathspec" 255 | version = "0.10.1" 256 | description = "Utility library for gitignore style pattern matching of file paths." 257 | category = "dev" 258 | optional = false 259 | python-versions = ">=3.7" 260 | 261 | [[package]] 262 | name = "platformdirs" 263 | version = "2.5.2" 264 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 265 | category = "dev" 266 | optional = false 267 | python-versions = ">=3.7" 268 | 269 | [package.extras] 270 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] 271 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 272 | 273 | [[package]] 274 | name = "pluggy" 275 | version = "1.0.0" 276 | description = "plugin and hook calling mechanisms for python" 277 | category = "dev" 278 | optional = false 279 | python-versions = ">=3.6" 280 | 281 | [package.extras] 282 | dev = ["pre-commit", "tox"] 283 | testing = ["pytest", "pytest-benchmark"] 284 | 285 | [[package]] 286 | name = "py" 287 | version = "1.11.0" 288 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 289 | category = "dev" 290 | optional = false 291 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 292 | 293 | [[package]] 294 | name = "pycodestyle" 295 | version = "2.9.1" 296 | description = "Python style guide checker" 297 | category = "dev" 298 | optional = false 299 | python-versions = ">=3.6" 300 | 301 | [[package]] 302 | name = "pydantic" 303 | version = "1.10.2" 304 | description = "Data validation and settings management using python type hints" 305 | category = "dev" 306 | optional = false 307 | python-versions = ">=3.7" 308 | 309 | [package.dependencies] 310 | typing-extensions = ">=4.1.0" 311 | 312 | [package.extras] 313 | dotenv = ["python-dotenv (>=0.10.4)"] 314 | email = ["email-validator (>=1.0.3)"] 315 | 316 | [[package]] 317 | name = "pyflakes" 318 | version = "2.5.0" 319 | description = "passive checker of Python programs" 320 | category = "dev" 321 | optional = false 322 | python-versions = ">=3.6" 323 | 324 | [[package]] 325 | name = "Pygments" 326 | version = "2.13.0" 327 | description = "Pygments is a syntax highlighting package written in Python." 328 | category = "dev" 329 | optional = false 330 | python-versions = ">=3.6" 331 | 332 | [package.extras] 333 | plugins = ["importlib-metadata"] 334 | 335 | [[package]] 336 | name = "pyparsing" 337 | version = "3.0.9" 338 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 339 | category = "dev" 340 | optional = false 341 | python-versions = ">=3.6.8" 342 | 343 | [package.extras] 344 | diagrams = ["jinja2", "railroad-diagrams"] 345 | 346 | [[package]] 347 | name = "pytest" 348 | version = "7.1.3" 349 | description = "pytest: simple powerful testing with Python" 350 | category = "dev" 351 | optional = false 352 | python-versions = ">=3.7" 353 | 354 | [package.dependencies] 355 | attrs = ">=19.2.0" 356 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 357 | iniconfig = "*" 358 | packaging = "*" 359 | pluggy = ">=0.12,<2.0" 360 | py = ">=1.8.2" 361 | tomli = ">=1.0.0" 362 | 363 | [package.extras] 364 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 365 | 366 | [[package]] 367 | name = "pytest-black" 368 | version = "0.3.12" 369 | description = "A pytest plugin to enable format checking with black" 370 | category = "dev" 371 | optional = false 372 | python-versions = ">=2.7" 373 | 374 | [package.dependencies] 375 | black = {version = "*", markers = "python_version >= \"3.6\""} 376 | pytest = ">=3.5.0" 377 | toml = "*" 378 | 379 | [[package]] 380 | name = "pytest-flake8" 381 | version = "1.1.1" 382 | description = "pytest plugin to check FLAKE8 requirements" 383 | category = "dev" 384 | optional = false 385 | python-versions = "*" 386 | 387 | [package.dependencies] 388 | flake8 = ">=4.0" 389 | pytest = ">=7.0" 390 | 391 | [[package]] 392 | name = "pytest-isort" 393 | version = "3.0.0" 394 | description = "py.test plugin to check import ordering using isort" 395 | category = "dev" 396 | optional = false 397 | python-versions = ">=3.6,<4" 398 | 399 | [package.dependencies] 400 | isort = ">=4.0" 401 | pytest = ">=5.0" 402 | 403 | [[package]] 404 | name = "pytest-mypy" 405 | version = "0.10.0" 406 | description = "Mypy static type checker plugin for Pytest" 407 | category = "dev" 408 | optional = false 409 | python-versions = ">=3.6" 410 | 411 | [package.dependencies] 412 | attrs = ">=19.0" 413 | filelock = ">=3.0" 414 | mypy = {version = ">=0.780", markers = "python_version >= \"3.9\""} 415 | pytest = {version = ">=6.2", markers = "python_version >= \"3.10\""} 416 | 417 | [[package]] 418 | name = "pytz" 419 | version = "2022.4" 420 | description = "World timezone definitions, modern and historical" 421 | category = "dev" 422 | optional = false 423 | python-versions = "*" 424 | 425 | [[package]] 426 | name = "requests" 427 | version = "2.28.1" 428 | description = "Python HTTP for Humans." 429 | category = "dev" 430 | optional = false 431 | python-versions = ">=3.7, <4" 432 | 433 | [package.dependencies] 434 | certifi = ">=2017.4.17" 435 | charset-normalizer = ">=2,<3" 436 | idna = ">=2.5,<4" 437 | urllib3 = ">=1.21.1,<1.27" 438 | 439 | [package.extras] 440 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 441 | use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] 442 | 443 | [[package]] 444 | name = "rich" 445 | version = "12.6.0" 446 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 447 | category = "dev" 448 | optional = false 449 | python-versions = ">=3.6.3,<4.0.0" 450 | 451 | [package.dependencies] 452 | commonmark = ">=0.9.0,<0.10.0" 453 | pygments = ">=2.6.0,<3.0.0" 454 | 455 | [package.extras] 456 | jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] 457 | 458 | [[package]] 459 | name = "rstcheck" 460 | version = "6.1.0" 461 | description = "Checks syntax of reStructuredText and code blocks nested within it" 462 | category = "dev" 463 | optional = false 464 | python-versions = ">=3.7,<4.0" 465 | 466 | [package.dependencies] 467 | docutils = ">=0.7,<0.19" 468 | pydantic = ">=1.2,<2.0" 469 | rstcheck-core = ">=1.0.2,<2.0.0" 470 | typer = {version = ">=0.4.1,<0.7", extras = ["all"]} 471 | types-docutils = ">=0.18,<0.19" 472 | 473 | [package.extras] 474 | docs = ["m2r2 (>=0.3.2)", "sphinx", "sphinx-autobuild (==2021.3.14)", "sphinx-click (>=4.0.3,<5.0.0)", "sphinx-rtd-dark-mode (>=1.2.4,<2.0.0)", "sphinx-rtd-theme (<1)", "sphinxcontrib-spelling (>=7.3)"] 475 | sphinx = ["sphinx"] 476 | testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=6.0)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] 477 | toml = ["tomli"] 478 | 479 | [[package]] 480 | name = "rstcheck-core" 481 | version = "1.0.2" 482 | description = "Checks syntax of reStructuredText and code blocks nested within it" 483 | category = "dev" 484 | optional = false 485 | python-versions = ">=3.7,<4.0" 486 | 487 | [package.dependencies] 488 | docutils = ">=0.7,<0.19" 489 | pydantic = ">=1.2,<2.0" 490 | types-docutils = ">=0.18,<0.19" 491 | 492 | [package.extras] 493 | docs = ["m2r2 (>=0.3.2)", "sphinx (>=4.0,<6.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-autodoc-typehints (>=1.15)", "sphinx-rtd-dark-mode (>=1.2.4,<2.0.0)", "sphinx-rtd-theme (<1)", "sphinxcontrib-apidoc (>=0.3)", "sphinxcontrib-spelling (>=7.3)"] 494 | sphinx = ["sphinx (>=4.0,<6.0)"] 495 | testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "mock (>=4.0)", "pytest (>=6.0)", "pytest-cov (>=3.0)", "pytest-mock (>=3.7)", "pytest-randomly (>=3.0)"] 496 | toml = ["tomli (>=2.0,<3.0)"] 497 | 498 | [[package]] 499 | name = "shellingham" 500 | version = "1.5.0" 501 | description = "Tool to Detect Surrounding Shell" 502 | category = "dev" 503 | optional = false 504 | python-versions = ">=3.4" 505 | 506 | [[package]] 507 | name = "snowballstemmer" 508 | version = "2.2.0" 509 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 510 | category = "dev" 511 | optional = false 512 | python-versions = "*" 513 | 514 | [[package]] 515 | name = "Sphinx" 516 | version = "5.2.3" 517 | description = "Python documentation generator" 518 | category = "dev" 519 | optional = false 520 | python-versions = ">=3.6" 521 | 522 | [package.dependencies] 523 | alabaster = ">=0.7,<0.8" 524 | babel = ">=2.9" 525 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 526 | docutils = ">=0.14,<0.20" 527 | imagesize = ">=1.3" 528 | Jinja2 = ">=3.0" 529 | packaging = ">=21.0" 530 | Pygments = ">=2.12" 531 | requests = ">=2.5.0" 532 | snowballstemmer = ">=2.0" 533 | sphinxcontrib-applehelp = "*" 534 | sphinxcontrib-devhelp = "*" 535 | sphinxcontrib-htmlhelp = ">=2.0.0" 536 | sphinxcontrib-jsmath = "*" 537 | sphinxcontrib-qthelp = "*" 538 | sphinxcontrib-serializinghtml = ">=1.1.5" 539 | 540 | [package.extras] 541 | docs = ["sphinxcontrib-websupport"] 542 | lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] 543 | test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] 544 | 545 | [[package]] 546 | name = "sphinxcontrib-applehelp" 547 | version = "1.0.2" 548 | description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" 549 | category = "dev" 550 | optional = false 551 | python-versions = ">=3.5" 552 | 553 | [package.extras] 554 | lint = ["docutils-stubs", "flake8", "mypy"] 555 | test = ["pytest"] 556 | 557 | [[package]] 558 | name = "sphinxcontrib-devhelp" 559 | version = "1.0.2" 560 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." 561 | category = "dev" 562 | optional = false 563 | python-versions = ">=3.5" 564 | 565 | [package.extras] 566 | lint = ["docutils-stubs", "flake8", "mypy"] 567 | test = ["pytest"] 568 | 569 | [[package]] 570 | name = "sphinxcontrib-htmlhelp" 571 | version = "2.0.0" 572 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 573 | category = "dev" 574 | optional = false 575 | python-versions = ">=3.6" 576 | 577 | [package.extras] 578 | lint = ["docutils-stubs", "flake8", "mypy"] 579 | test = ["html5lib", "pytest"] 580 | 581 | [[package]] 582 | name = "sphinxcontrib-jsmath" 583 | version = "1.0.1" 584 | description = "A sphinx extension which renders display math in HTML via JavaScript" 585 | category = "dev" 586 | optional = false 587 | python-versions = ">=3.5" 588 | 589 | [package.extras] 590 | test = ["flake8", "mypy", "pytest"] 591 | 592 | [[package]] 593 | name = "sphinxcontrib-qthelp" 594 | version = "1.0.3" 595 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." 596 | category = "dev" 597 | optional = false 598 | python-versions = ">=3.5" 599 | 600 | [package.extras] 601 | lint = ["docutils-stubs", "flake8", "mypy"] 602 | test = ["pytest"] 603 | 604 | [[package]] 605 | name = "sphinxcontrib-serializinghtml" 606 | version = "1.1.5" 607 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." 608 | category = "dev" 609 | optional = false 610 | python-versions = ">=3.5" 611 | 612 | [package.extras] 613 | lint = ["docutils-stubs", "flake8", "mypy"] 614 | test = ["pytest"] 615 | 616 | [[package]] 617 | name = "toml" 618 | version = "0.10.2" 619 | description = "Python Library for Tom's Obvious, Minimal Language" 620 | category = "dev" 621 | optional = false 622 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 623 | 624 | [[package]] 625 | name = "tomli" 626 | version = "2.0.1" 627 | description = "A lil' TOML parser" 628 | category = "dev" 629 | optional = false 630 | python-versions = ">=3.7" 631 | 632 | [[package]] 633 | name = "typer" 634 | version = "0.6.1" 635 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 636 | category = "dev" 637 | optional = false 638 | python-versions = ">=3.6" 639 | 640 | [package.dependencies] 641 | click = ">=7.1.1,<9.0.0" 642 | colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} 643 | rich = {version = ">=10.11.0,<13.0.0", optional = true, markers = "extra == \"all\""} 644 | shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} 645 | 646 | [package.extras] 647 | all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 648 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] 649 | doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)"] 650 | test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 651 | 652 | [[package]] 653 | name = "types-docutils" 654 | version = "0.18.3" 655 | description = "Typing stubs for docutils" 656 | category = "dev" 657 | optional = false 658 | python-versions = "*" 659 | 660 | [[package]] 661 | name = "typing-extensions" 662 | version = "4.4.0" 663 | description = "Backported and Experimental Type Hints for Python 3.7+" 664 | category = "dev" 665 | optional = false 666 | python-versions = ">=3.7" 667 | 668 | [[package]] 669 | name = "urllib3" 670 | version = "1.26.12" 671 | description = "HTTP library with thread-safe connection pooling, file post, and more." 672 | category = "dev" 673 | optional = false 674 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" 675 | 676 | [package.extras] 677 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 678 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 679 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 680 | 681 | [metadata] 682 | lock-version = "1.1" 683 | python-versions = "^3.10" 684 | content-hash = "a4d2697ee0c07182f61b58d6fe9091744e1cd2094650adf421c67c2bb0eb9279" 685 | 686 | [metadata.files] 687 | alabaster = [ 688 | {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, 689 | {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, 690 | ] 691 | attrs = [ 692 | {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, 693 | {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, 694 | ] 695 | Babel = [ 696 | {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, 697 | {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, 698 | ] 699 | black = [ 700 | {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, 701 | {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, 702 | {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, 703 | {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, 704 | {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, 705 | {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, 706 | {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, 707 | {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, 708 | {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, 709 | {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, 710 | {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, 711 | {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, 712 | ] 713 | certifi = [ 714 | {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, 715 | {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, 716 | ] 717 | charset-normalizer = [ 718 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 719 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 720 | ] 721 | click = [ 722 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 723 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 724 | ] 725 | colorama = [ 726 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 727 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 728 | ] 729 | commonmark = [ 730 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 731 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 732 | ] 733 | docutils = [ 734 | {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, 735 | {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, 736 | ] 737 | filelock = [ 738 | {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, 739 | {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, 740 | ] 741 | flake8 = [ 742 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 743 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 744 | ] 745 | idna = [ 746 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 747 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 748 | ] 749 | imagesize = [ 750 | {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, 751 | {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, 752 | ] 753 | iniconfig = [ 754 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 755 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 756 | ] 757 | isort = [ 758 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 759 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 760 | ] 761 | Jinja2 = [ 762 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, 763 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, 764 | ] 765 | MarkupSafe = [ 766 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, 767 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, 768 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, 769 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, 770 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, 771 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, 772 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, 773 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, 774 | {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, 775 | {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, 776 | {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, 777 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, 778 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, 779 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, 780 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, 781 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, 782 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, 783 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, 784 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, 785 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, 786 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, 787 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, 788 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, 789 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, 790 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, 791 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, 792 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, 793 | {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, 794 | {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, 795 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, 796 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, 797 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, 798 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, 799 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, 800 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, 801 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, 802 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, 803 | {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, 804 | {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, 805 | {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, 806 | ] 807 | mccabe = [ 808 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 809 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 810 | ] 811 | mypy = [ 812 | {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, 813 | {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, 814 | {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"}, 815 | {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"}, 816 | {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"}, 817 | {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"}, 818 | {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"}, 819 | {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"}, 820 | {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"}, 821 | {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"}, 822 | {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"}, 823 | {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"}, 824 | {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"}, 825 | {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"}, 826 | {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"}, 827 | {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"}, 828 | {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"}, 829 | {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"}, 830 | {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"}, 831 | {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"}, 832 | {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"}, 833 | {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"}, 834 | {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"}, 835 | {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"}, 836 | ] 837 | mypy-extensions = [ 838 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 839 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 840 | ] 841 | ordered-set = [ 842 | {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, 843 | {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, 844 | ] 845 | packaging = [ 846 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 847 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 848 | ] 849 | pathspec = [ 850 | {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, 851 | {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, 852 | ] 853 | platformdirs = [ 854 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 855 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 856 | ] 857 | pluggy = [ 858 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 859 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 860 | ] 861 | py = [ 862 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 863 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 864 | ] 865 | pycodestyle = [ 866 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 867 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 868 | ] 869 | pydantic = [ 870 | {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, 871 | {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, 872 | {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, 873 | {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, 874 | {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, 875 | {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, 876 | {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, 877 | {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, 878 | {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, 879 | {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, 880 | {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, 881 | {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, 882 | {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, 883 | {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, 884 | {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, 885 | {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, 886 | {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, 887 | {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, 888 | {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, 889 | {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, 890 | {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, 891 | {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, 892 | {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, 893 | {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, 894 | {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, 895 | {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, 896 | {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, 897 | {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, 898 | {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, 899 | {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, 900 | {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, 901 | {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, 902 | {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, 903 | {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, 904 | {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, 905 | {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, 906 | ] 907 | pyflakes = [ 908 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 909 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 910 | ] 911 | Pygments = [ 912 | {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, 913 | {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, 914 | ] 915 | pyparsing = [ 916 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 917 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 918 | ] 919 | pytest = [ 920 | {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, 921 | {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, 922 | ] 923 | pytest-black = [ 924 | {file = "pytest-black-0.3.12.tar.gz", hash = "sha256:1d339b004f764d6cd0f06e690f6dd748df3d62e6fe1a692d6a5500ac2c5b75a5"}, 925 | ] 926 | pytest-flake8 = [ 927 | {file = "pytest-flake8-1.1.1.tar.gz", hash = "sha256:ba4f243de3cb4c2486ed9e70752c80dd4b636f7ccb27d4eba763c35ed0cd316e"}, 928 | {file = "pytest_flake8-1.1.1-py2.py3-none-any.whl", hash = "sha256:e0661a786f8cbf976c185f706fdaf5d6df0b1667c3bcff8e823ba263618627e7"}, 929 | ] 930 | pytest-isort = [ 931 | {file = "pytest-isort-3.0.0.tar.gz", hash = "sha256:4fe4b26ead2af776730ec23f5870d7421f35aace22a41c4e938586ef4d8787cb"}, 932 | {file = "pytest_isort-3.0.0-py3-none-any.whl", hash = "sha256:2d96a25a135d6fd084ac36878e7d54f26f27c6987c2c65f0d12809bffade9cb9"}, 933 | ] 934 | pytest-mypy = [ 935 | {file = "pytest-mypy-0.10.0.tar.gz", hash = "sha256:e74d632685f15a39c31c551a9d8cec4619e24bd396245a6335c5db0ec6d17b6f"}, 936 | {file = "pytest_mypy-0.10.0-py3-none-any.whl", hash = "sha256:83843dce75a7ce055efb264ff40dad2ecf7abd4e7bd5e5eda015261d11616abb"}, 937 | ] 938 | pytz = [ 939 | {file = "pytz-2022.4-py2.py3-none-any.whl", hash = "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91"}, 940 | {file = "pytz-2022.4.tar.gz", hash = "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174"}, 941 | ] 942 | requests = [ 943 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, 944 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, 945 | ] 946 | rich = [ 947 | {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, 948 | {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, 949 | ] 950 | rstcheck = [ 951 | {file = "rstcheck-6.1.0-py3-none-any.whl", hash = "sha256:156a63612ed58676b5686ddcde6bb3fe67b6ee090141483a92e56b5bb5f8b617"}, 952 | {file = "rstcheck-6.1.0.tar.gz", hash = "sha256:1d32f50d2131a43a39e0287d1b1854fcc0e98ab1513f0318df2ecab25bec54ab"}, 953 | ] 954 | rstcheck-core = [ 955 | {file = "rstcheck-core-1.0.2.tar.gz", hash = "sha256:b06bee11f3679b283cbd6abf707bfe4d5fd2cf480cfa3ffe6d5c9238c6d4ae89"}, 956 | {file = "rstcheck_core-1.0.2-py3-none-any.whl", hash = "sha256:6d75e858441644b9a0ad7a89a6c0a9012920be7dc4fad34f95579ae2a97a2d5e"}, 957 | ] 958 | shellingham = [ 959 | {file = "shellingham-1.5.0-py2.py3-none-any.whl", hash = "sha256:a8f02ba61b69baaa13facdba62908ca8690a94b8119b69f5ec5873ea85f7391b"}, 960 | {file = "shellingham-1.5.0.tar.gz", hash = "sha256:72fb7f5c63103ca2cb91b23dee0c71fe8ad6fbfd46418ef17dbe40db51592dad"}, 961 | ] 962 | snowballstemmer = [ 963 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 964 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 965 | ] 966 | Sphinx = [ 967 | {file = "Sphinx-5.2.3.tar.gz", hash = "sha256:5b10cb1022dac8c035f75767799c39217a05fc0fe2d6fe5597560d38e44f0363"}, 968 | {file = "sphinx-5.2.3-py3-none-any.whl", hash = "sha256:7abf6fabd7b58d0727b7317d5e2650ef68765bbe0ccb63c8795fa8683477eaa2"}, 969 | ] 970 | sphinxcontrib-applehelp = [ 971 | {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, 972 | {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, 973 | ] 974 | sphinxcontrib-devhelp = [ 975 | {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, 976 | {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, 977 | ] 978 | sphinxcontrib-htmlhelp = [ 979 | {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, 980 | {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, 981 | ] 982 | sphinxcontrib-jsmath = [ 983 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 984 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 985 | ] 986 | sphinxcontrib-qthelp = [ 987 | {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, 988 | {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, 989 | ] 990 | sphinxcontrib-serializinghtml = [ 991 | {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, 992 | {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, 993 | ] 994 | toml = [ 995 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 996 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 997 | ] 998 | tomli = [ 999 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1000 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1001 | ] 1002 | typer = [ 1003 | {file = "typer-0.6.1-py3-none-any.whl", hash = "sha256:54b19e5df18654070a82f8c2aa1da456a4ac16a2a83e6dcd9f170e291c56338e"}, 1004 | {file = "typer-0.6.1.tar.gz", hash = "sha256:2d5720a5e63f73eaf31edaa15f6ab87f35f0690f8ca233017d7d23d743a91d73"}, 1005 | ] 1006 | types-docutils = [ 1007 | {file = "types-docutils-0.18.3.tar.gz", hash = "sha256:a0ef831dc20635f350fa9cff591231c31d27e75771e59fd6c979b6c0c7e03292"}, 1008 | {file = "types_docutils-0.18.3-py3-none-any.whl", hash = "sha256:b54b6fd599914093a5aab08dbf1cba96eb107cdeb4210bbe4ccd233fe3a71d9b"}, 1009 | ] 1010 | typing-extensions = [ 1011 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, 1012 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, 1013 | ] 1014 | urllib3 = [ 1015 | {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, 1016 | {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, 1017 | ] 1018 | --------------------------------------------------------------------------------