From b7d1a7f04c637bf0dfae20dbafc6d4036e54ae9b Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Mon, 4 Oct 2021 10:23:40 -0400 Subject: [PATCH 01/26] mots: create initial prototype (bug 1733956) This includes: - makefile to assist with development environment - support for basic commands to parse and validate mots.yaml - support for modules and submodules - support for a directory and searchable index - cli interface - tests TODOs: - add more environment-specific variables to be by mots internally - add circleci and readthedocs configuration - improve documentation - improve readability of fixtures --- .coveragerc | 2 + .flake8 | 14 +++ AUTHORS | 1 + Makefile | 101 +++++++++++++++++ README.md | 51 +++++++++ documentation/Makefile | 20 ++++ documentation/conf.py | 57 ++++++++++ documentation/index.rst | 41 +++++++ pyproject.toml | 17 +++ requirements.in | 11 ++ requirements.txt | 174 ++++++++++++++++++++++++++++ setup.cfg | 34 ++++++ src/mots/__init__.py | 9 ++ src/mots/cli.py | 189 +++++++++++++++++++++++++++++++ src/mots/config.py | 55 +++++++++ src/mots/directory.py | 81 +++++++++++++ src/mots/logging.py | 54 +++++++++ src/mots/module.py | 244 ++++++++++++++++++++++++++++++++++++++++ src/mots/utils.py | 12 ++ tests/conftest.py | 109 ++++++++++++++++++ tests/test_directory.py | 98 ++++++++++++++++ tests/test_module.py | 137 ++++++++++++++++++++++ tests/test_style.py | 22 ++++ tests/test_utils.py | 15 +++ 24 files changed, 1548 insertions(+) create mode 100644 .coveragerc create mode 100644 .flake8 create mode 100644 AUTHORS create mode 100644 Makefile create mode 100644 README.md create mode 100644 documentation/Makefile create mode 100644 documentation/conf.py create mode 100644 documentation/index.rst create mode 100644 pyproject.toml create mode 100644 requirements.in create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 src/mots/__init__.py create mode 100644 src/mots/cli.py create mode 100644 src/mots/config.py create mode 100644 src/mots/directory.py create mode 100644 src/mots/logging.py create mode 100644 src/mots/module.py create mode 100644 src/mots/utils.py create mode 100644 tests/conftest.py create mode 100644 tests/test_directory.py create mode 100644 tests/test_module.py create mode 100644 tests/test_style.py create mode 100644 tests/test_utils.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..f3ef71f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +source = src/mots diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..3edf399 --- /dev/null +++ b/.flake8 @@ -0,0 +1,14 @@ +[flake8] +# Matching black's default +max-line-length = 88 + +extend-ignore = + D107, # Missing docstring in __init__ + +per-file-ignores = + # D100 Missing docstring in public module + # D101 Missing docstring in public class + # D102 Missing docstring in public method + # D103 Missing docstring in public function + # D104 Missing docstring in public package + tests/*: D100,D101,D102,D103,D104 diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..9b6a372 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Zeid Zabaneh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..345b8bb --- /dev/null +++ b/Makefile @@ -0,0 +1,101 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +SHELL := /bin/bash +PYTHON := $(shell which python) + + +.PHONY: help +help: + @echo "using python at $(PYTHON)." + @$(PYTHON) --version + @echo "usage: make " + @echo + @echo "target is one of:" + @echo " help show this message and exit" + @echo " build build the python package and wheels" + @echo " clean remove temporary files" + @echo " cov run coverage check" + @echo " cov-html generate html coverage report" + @echo " dev setup local dev environment by installing required packages, etc." + @echo " dev-env create a python virtual environment in ./mots-env" + @echo " docs generate documentation" + @echo " requirements regenerate requirements.txt" + @echo " serve-cov simple http server for coverage report" + @echo " serve-docs simple http server for docs" + @echo " test run the test suite" + +.PHONY: test +test: + $(PYTHON) -m pytest -vvv + +.PHONY: build +build: + $(PYTHON) -m build + +.PHONY: docs +docs: + make -C documentation html + +.PHONY: cov +cov: + -@rm -R .coverage 2>/dev/null ||: + $(PYTHON) -m pytest --cov-fail-under=50 --cov=src/mots tests + +.PHONY: cov-html +cov-html: + -@rm -R .coverage 2>/dev/null ||: + -@rm -R .htmlcov 2>/dev/null ||: + $(PYTHON) -m pytest --cov=src/mots tests --cov-report html + +.PHONY: serve-cov +serve-cov: + $(PYTHON) -m http.server -d htmlcov 8080 + +.PHONY: format +format: + $(PYTHON) -m black src/mots + $(PYTHON) -m black tests + +.PHONY: requirements +requirements: + $ rm requirements.txt + $(PYTHON) -m piptools compile + +.ONESHELL: +.PHONY: dev-env +dev-env: +# TODO Make this better. +ifdef PY + @echo "Creating virtual env in .mots-env using provided Python ($(PY))..." + -@(/usr/bin/$(PY) -m venv .mots-env && ([ $$? -eq 0 ] && echo "$(PY) found.")) 2>/dev/null|| + -@(/usr/local/bin/$(PY) -m venv .mots-env && ([ $$? -eq 0 ] && echo "$(PY) found.")) 2>/dev/null +else + @echo "PY variable not defined, please pass a python version (e.g. PY=python3.8)" +endif + +.PHONY: dev +dev: + # TODO: only run this if we can detect .mots-env virtual environment. + $(PYTHON) -m pip install --upgrade pip + $(PYTHON) -m pip install pip-tools + $(PYTHON) -m pip install -r requirements.txt + $(PYTHON) -m pip install -e . + +.PHONY: serve-docs +serve-docs: + $(PYTHON) -m http.server -d documentation/_build/html 8080 + +.PHONY: clean +clean: + -@rm -R htmlcov 2>/dev/null ||: + -@rm -R documentation/_build 2>/dev/null ||: + -@rm -R dist 2>/dev/null ||: + -@rm -R src/*.egg-info 2>/dev/null ||: + -@rm -R src/mots/__pycache__ 2>/dev/null ||: + -@rm -R tests/__pycache__ 2>/dev/null ||: + -@rm -R .coverage 2>/dev/null ||: + -@rm -R .pytest_cache 2>/dev/null ||: + -@rm -R .mots-env 2>/dev/null ||: + -@rm -R .pytest_cache-env 2>/dev/null ||: diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7eb1dd --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +Development environment +----------------------- +To set up a local development environment, run the following commands. Replace the python version with the desired version on your local machine. + +.. code-block:: bash + + make dev-env PY=python3.9 + source .mots-env/bin/activate + make dev + +The above commands will set up a local development environment using the provided python version available on your machine, and subsequently install all required packages in that environment. + +Generate coverage report +------------------------ + +To generate a standard coverage report, run: + +.. code-block:: bash + + make cov + +To generate an html coverage report, run: + +.. code-block:: bash + + make cov-html + make serve-cov + +Then navigate to your web browser. + +Other make commands +------------------- +Run `make` to see all available commands. + +.. code-block:: bash + + usage: make + + target is one of: + help show this message and exit + build build the python package and wheels + clean remove temporary files + cov run coverage check + cov-html generate html coverage report + dev setup local dev environment by installing required packages, etc. + dev-env create a python virtual environment in ./mots-env + docs generate documentation + requirements regenerate requirements.txt + serve-cov simple http server for coverage report + serve-docs simple http server for docs + test run the test suite diff --git a/documentation/Makefile b/documentation/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/documentation/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/documentation/conf.py b/documentation/conf.py new file mode 100644 index 0000000..94b8499 --- /dev/null +++ b/documentation/conf.py @@ -0,0 +1,57 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Configuration file for the Sphinx documentation builder.""" + +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../src')) + + +# -- Project information ----------------------------------------------------- + +project = 'mots' +copyright = '2021 Mozilla Inc.' +author = 'Zeid Zabaneh ' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/documentation/index.rst b/documentation/index.rst new file mode 100644 index 0000000..23ae75a --- /dev/null +++ b/documentation/index.rst @@ -0,0 +1,41 @@ +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + +README +====== + +.. include:: ../README.md + +Developer Interface +=================== + +Module +------ +.. automodule:: mots.module + :members: + + +Directory +--------- +.. automodule:: mots.directory + :members: + + +CLI +--------- +.. automodule:: mots.cli + :members: + + +Config +--------- +.. automodule:: mots.config + :members: + + +Utils +--------- +.. automodule:: mots.utils + :members: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7ba632b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" +testpaths = [ + "tests", +] diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..7529886 --- /dev/null +++ b/requirements.in @@ -0,0 +1,11 @@ +black==21.7b0 +build==0.6.0.post1 +flake8-docstrings==1.6.0 +flake8==3.9.2 +pytest-cov==2.12.1 +pytest==6.2.5 +pyyaml==5.4.1 +ruamel.yaml==0.17.16 +sphinx-rtd-theme==1.0.0 +sphinx==4.2.0 +twine==3.4.2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..acda099 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,174 @@ +# +# This file is autogenerated by pip-compile with python 3.8 +# To update, run: +# +# pip-compile +# +alabaster==0.7.12 + # via sphinx +appdirs==1.4.4 + # via black +attrs==21.2.0 + # via pytest +babel==2.9.1 + # via sphinx +black==21.7b0 + # via -r requirements.in +bleach==4.1.0 + # via readme-renderer +build==0.6.0.post1 + # via -r requirements.in +certifi==2021.5.30 + # via requests +cffi==1.14.6 + # via cryptography +charset-normalizer==2.0.6 + # via requests +click==8.0.1 + # via black +colorama==0.4.4 + # via twine +coverage==6.0 + # via pytest-cov +cryptography==35.0.0 + # via secretstorage +docutils==0.17.1 + # via + # readme-renderer + # sphinx + # sphinx-rtd-theme +flake8==3.9.2 + # via + # -r requirements.in + # flake8-docstrings +flake8-docstrings==1.6.0 + # via -r requirements.in +idna==3.2 + # via requests +imagesize==1.2.0 + # via sphinx +importlib-metadata==4.8.1 + # via + # keyring + # twine +iniconfig==1.1.1 + # via pytest +jeepney==0.7.1 + # via + # keyring + # secretstorage +jinja2==3.0.1 + # via sphinx +keyring==23.2.1 + # via twine +markupsafe==2.0.1 + # via jinja2 +mccabe==0.6.1 + # via flake8 +mypy-extensions==0.4.3 + # via black +packaging==21.0 + # via + # bleach + # build + # pytest + # sphinx +pathspec==0.9.0 + # via black +pep517==0.11.0 + # via build +pkginfo==1.7.1 + # via twine +pluggy==1.0.0 + # via pytest +py==1.10.0 + # via pytest +pycodestyle==2.7.0 + # via flake8 +pycparser==2.20 + # via cffi +pydocstyle==6.1.1 + # via flake8-docstrings +pyflakes==2.3.1 + # via flake8 +pygments==2.10.0 + # via + # readme-renderer + # sphinx +pyparsing==2.4.7 + # via packaging +pytest==6.2.5 + # via + # -r requirements.in + # pytest-cov +pytest-cov==2.12.1 + # via -r requirements.in +pytz==2021.3 + # via babel +pyyaml==5.4.1 + # via -r requirements.in +readme-renderer==30.0 + # via twine +regex==2021.9.30 + # via black +requests==2.26.0 + # via + # requests-toolbelt + # sphinx + # twine +requests-toolbelt==0.9.1 + # via twine +rfc3986==1.5.0 + # via twine +ruamel.yaml==0.17.16 + # via -r requirements.in +ruamel.yaml.clib==0.2.6 + # via ruamel.yaml +secretstorage==3.3.1 + # via keyring +six==1.16.0 + # via bleach +snowballstemmer==2.1.0 + # via + # pydocstyle + # sphinx +sphinx==4.2.0 + # via + # -r requirements.in + # sphinx-rtd-theme +sphinx-rtd-theme==1.0.0 + # via -r requirements.in +sphinxcontrib-applehelp==1.0.2 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.0 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 + # via sphinx +toml==0.10.2 + # via + # pytest + # pytest-cov +tomli==1.2.1 + # via + # black + # build + # pep517 +tqdm==4.62.3 + # via twine +twine==3.4.2 + # via -r requirements.in +urllib3==1.26.7 + # via requests +webencodings==0.5.1 + # via bleach +zipp==3.6.0 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..d21da6c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,34 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +[metadata] +name = mots +version = 0.0.1a0 +author = Zeid Zabaneh +author_email = zeid@mozilla.com +description = Module Ownership in Tree System +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/mozilla-conduit/mots +classifiers = + License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) + Development Status :: 1 - Planning + Intended Audience :: Developers + Operating System :: OS Independent + Programming Language :: Python :: 3 + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.7 +install_requires = + ruamel.yaml==0.17.16 + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + mots = mots.cli:main diff --git a/src/mots/__init__.py b/src/mots/__init__.py new file mode 100644 index 0000000..d375bc5 --- /dev/null +++ b/src/mots/__init__.py @@ -0,0 +1,9 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +""" +Mots is the in-tree module ownership system. + +This library helps you track who owns what parts of the source code! +""" diff --git a/src/mots/cli.py b/src/mots/cli.py new file mode 100644 index 0000000..393cc78 --- /dev/null +++ b/src/mots/cli.py @@ -0,0 +1,189 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +""" +usage: mots [-h] [--debug] {init,module,validate,clean,query} ... + +main command line interface for mots + +optional arguments: + -h, --help show this help message and exit + --debug enable debug output + +commands: + {init,module,validate,clean,query} + init initialize mots configuration in repo + module module_operations + validate validate mots config for current repo + clean clean mots config for current repo + query query the module directory +""" +import argparse +from datetime import datetime +import logging +import sys +from pathlib import Path + +from mots import module +from mots.config import FileConfig +from mots.directory import Directory +from mots.logging import init_logging +from mots.config import DEFAULT_CONFIG_FILEPATH + +logger = logging.getLogger(__name__) + + +class CLI: + """Wrap methods and call with the correct arguments.""" + + @staticmethod + def init(args): + """Call `file_config.init` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.init() + + @staticmethod + def ls(args): + """Call `module.ls` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + return module.ls(file_config.config["modules"]) + + @staticmethod + def show(args): + """Call `module.show` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + return module.show(file_config.config["modules"], args.module) + + @staticmethod + def validate(args): + """Call `module.validate` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + return module.validate(file_config.config, args.repo_path) + + @staticmethod + def clean(args): + """Call `module.clean` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + return module.clean(file_config) + + @staticmethod + def query(args): + """Call `directory.query` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + directory = Directory(file_config) + directory.load() + result = directory.query(*args.paths) + for path in result: + modules = result[path] + module_names = ",".join([m.machine_name for m in modules]) + owners = ",".join([",".join(m.owners) for m in modules]) + sys.stdout.write(f"{path}:{module_names}:{owners}\n") + + +def main(): + """Redirect to appropriate function.""" + parser = create_parser() + args = parser.parse_args() + + init_logging(debug=args.debug) + + if hasattr(args, "func"): + st = datetime.now() + args.func(args) + et = datetime.now() + logger.debug(f"{args.func} took {(et - st).total_seconds()} seconds.") + else: + parser.print_help() + + +def create_parser(): + """Create parser, subparsers, and arguments.""" + parser = argparse.ArgumentParser(description="main command line interface for mots") + parser.add_argument("--debug", action="store_true", help="enable debug output") + subparsers = parser.add_subparsers(title="commands") + + init_parser = subparsers.add_parser( + "init", help="initialize mots configuration in repo" + ) + init_parser.add_argument( + "--path", + "-p", + type=str, + help="the path of the repo to initialize", + default=DEFAULT_CONFIG_FILEPATH, + ) + init_parser.set_defaults(func=CLI.init) + + module_parser = subparsers.add_parser("module", help="module_operations") + module_parser.add_argument( + "--path", + "-p", + type=str, + help="the path of the repo config file", + default=DEFAULT_CONFIG_FILEPATH, + ) + module_parsers = module_parser.add_subparsers(title="module") + + list_parser = module_parsers.add_parser("list", help="list all modules") + list_parser.set_defaults(func=CLI.ls) + + show_parser = module_parsers.add_parser("show", help="show a module") + show_parser.add_argument("module", help="name of the module to show") + show_parser.set_defaults(func=CLI.show) + + validate_parser = subparsers.add_parser( + "validate", help="validate mots config for current repo" + ) + validate_parser.add_argument( + "--path", + "-p", + type=str, + help="the path of the repo config file", + default=DEFAULT_CONFIG_FILEPATH, + ) + validate_parser.add_argument( + "--repo-path", + "-r", + type=str, + help="the path of the repo", + default=".", + ) + validate_parser.set_defaults(func=CLI.validate) + + clean_parser = subparsers.add_parser( + "clean", help="clean mots config for current repo" + ) + clean_parser.add_argument( + "--path", + "-p", + type=str, + help="the path of the repo config file", + default=DEFAULT_CONFIG_FILEPATH, + ) + clean_parser.add_argument( + "--repo-path", + "-r", + type=str, + help="the path of the repo", + default=".", + ) + clean_parser.set_defaults(func=CLI.clean) + + query_parser = subparsers.add_parser("query", help="query the module directory") + query_parser.add_argument( + "--path", + "-p", + type=str, + help="the path of the repo config file", + default=DEFAULT_CONFIG_FILEPATH, + ) + query_parser.add_argument("paths", nargs="+", help="a list of paths to query") + query_parser.set_defaults(func=CLI.query) + + return parser diff --git a/src/mots/config.py b/src/mots/config.py new file mode 100644 index 0000000..9ccb7b8 --- /dev/null +++ b/src/mots/config.py @@ -0,0 +1,55 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Configuration classes used to initialize and manage mots in a repo.""" +import logging + +from datetime import datetime +from pathlib import Path +from ruamel.yaml import YAML + +logger = logging.getLogger(__name__) + +yaml = YAML() + +DEFAULT_CONFIG_FILEPATH = "./mots.yaml" + + +class FileConfig: + """Loader and writer for filesystem based configuration.""" + + def __init__(self, path: Path = DEFAULT_CONFIG_FILEPATH): + """Initialize the configuration with provided config file path.""" + if not path.exists() and path.is_file(): + raise ValueError(f"{path} does not exist or is not a file.") + self.path = path + self.repo_path = path.parent + self.config = None + + def init(self): + """Initialize a repo with a config file, if it does not contain it.""" + if not self.path.is_file(): + # File does not exist, create it. + now = datetime.now() + self.config = { + "repo": str(Path(self.path).resolve().parts[-1]), + "created_at": now, + "updated_at": None, + "modules": None, + } + self.write() + logger.info(f"mots configuration initialized in {self.path}.") + else: + logger.warning(f"mots configuration file detected in {self.path}.") + + def load(self): + """Load configuration from file.""" + with open(self.path, "r") as f: + self.config = yaml.load(f) + + def write(self): + """Write configuration to file, and update the timestamp.""" + self.config["updated_at"] = datetime.now() + with open(self.path, "w") as f: + yaml.dump(self.config, f) diff --git a/src/mots/directory.py b/src/mots/directory.py new file mode 100644 index 0000000..f47f2b0 --- /dev/null +++ b/src/mots/directory.py @@ -0,0 +1,81 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Directory classes for mots.""" + +from __future__ import annotations +from collections import defaultdict +import logging +from mots.module import Module +from mots.config import FileConfig + +logger = logging.getLogger(__name__) + + +class Directory: + """Mots directory and path index.""" + + def __init__(self, config: FileConfig): + self.config_handle = config + self.config_handle.load() + + self.repo_path = self.config_handle.repo_path + self.modules = [ + Module(repo_path=self.repo_path, **m) + for m in self.config_handle.config["modules"] + ] + + self.modules_by_machine_name = {} + for module in self.modules: + self.modules_by_machine_name[module.machine_name] = module + + self.index = None + + def load(self, full_paths: bool = False): + """Load all paths in each module and put them in index.""" + self.index = defaultdict(list) + + if full_paths: + # Make sure all existing paths have an entry in the index. + logger.debug("Loading paths into index...") + for path in self.repo_path.glob("**/*"): + if path.relative_to(self.repo_path).parts[0] in (".hg", ".git"): + continue + self.index[path] = list() + logger.debug(f"{len(self.index)} paths loaded.") + + for module in self.modules: + if module.submodules: + # Start with more specific submodule definitions if present. + for submodule in module.submodules: + logger.debug(f"Updating index for {submodule.machine_name}...") + for path in submodule.calculate_paths(): + self.index[path].append(submodule) + logger.debug(f"Updating index for {module.machine_name}...") + # Add broader module definitions. + for path in module.calculate_paths(): + self.index[path].append(module) + + # Filter out modules that specifically exclude paths defined in other modules. + for path in self.index.keys(): + modules = self.index[path] + if len(modules) > 1: + self.index[path] = [m for m in modules if not m.exclude_module_paths] + self.index = dict(self.index) + + def query(self, *paths: str) -> tuple[list[Module], list[str]]: + """Query given paths and return a list of corresponding modules.""" + result = {} + rejected = [] + for path in paths: + if not (self.repo_path / path).exists(): + logger.warning(f"Path {path} does not exist, skipping.") + rejected.append(path) + continue + result[path] = self.index.get(self.repo_path / path, list()) + logger.info(f"Query {paths} resolved to {result}.") + + # NOTE: for new files, we need to use the old config against the new file + # structure. + return result, rejected diff --git a/src/mots/logging.py b/src/mots/logging.py new file mode 100644 index 0000000..67edeeb --- /dev/null +++ b/src/mots/logging.py @@ -0,0 +1,54 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Set up logging for mots.""" +import logging +import logging.config +from pathlib import Path + +logger = logging.getLogger(__name__) + +LOG_FILE_PATH = Path("mots.log") +LOG_MAX_SIZE = 1024 * 1024 * 50 +LOG_BACKUPS = 5 + + +def init_logging( + debug: bool = False, + path: Path = LOG_FILE_PATH, + log_max_size: int = LOG_MAX_SIZE, + log_backups: int = LOG_BACKUPS, +): + """Initialize default logger.""" + logging.config.dictConfig( + { + "version": 1, + "formatters": { + "standard": { + "format": "%(asctime)-13s %(module)-10s %(levelname)-8s %(message)s" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "standard", + }, + "file": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "standard", + "filename": path, + "maxBytes": log_max_size, + "backupCount": log_backups, + "encoding": "utf8", + }, + }, + "loggers": { + "mots": { + "level": logging.DEBUG if debug else logging.WARNING, + "handlers": ["console", "file"], + }, + }, + } + ) + logger.debug(f"Logging configured with log file @ {path}.") diff --git a/src/mots/module.py b/src/mots/module.py new file mode 100644 index 0000000..6e78388 --- /dev/null +++ b/src/mots/module.py @@ -0,0 +1,244 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Module operations and utility functions.""" + +from __future__ import annotations +from collections import defaultdict +import logging +from pathlib import Path +from mots.utils import generate_machine_readable_name +from mots.config import FileConfig + +logger = logging.getLogger(__name__) + + +class ValidationError(TypeError): + """Thrown when a particular module is not valid.""" + + pass + + +class Module: + """A top-level module or a submodule. + + :param machine_name: a unique, machine-readable name for the module + :param repo_path: the path of the top-level repository + :param name: the name of the module + :param includes: a list of paths (glob format) to include + :param excludes: a list of paths (glob format) to exclude + :param owners: a list of owners that will own all paths in this module + :param meta: a dictionary of meta data related to this module + :param parent: the parent module of this module, if this is a submodule + :param submodules: a list of submodules of this module + :param exclude_submodule_paths: when True, paths in submodules are excluded + :param exclude_module_paths: when True, common paths in other modules are excluded + from the directory index + """ + + def __repr__(self): + """Return a human readable name for the instance.""" + return f"" + + def __init__( + self, + machine_name: str, + repo_path: str, + name: str = None, + includes: str = None, + excludes: str = None, + owners: list[str] = None, + meta: dict = None, + parent: "Module" = None, + submodules: list[dict] = None, + exclude_submodule_paths: bool = True, + exclude_module_paths: bool = False, + ): + self.name = name + self.machine_name = machine_name + self.repo_path = Path(repo_path) + self.parent = parent + + self.excludes = excludes or [] + self.includes = includes or [] + self.owners = owners or [] + self.submodules = [] + self.exclude_submodule_paths = exclude_submodule_paths + self.exclude_module_paths = exclude_module_paths + self.errors = [] + + if submodules: + for module in submodules: + self.submodules.append( + Module(parent=self, repo_path=repo_path, **module) + ) + + self.meta = meta + + # `includes`, `excludes`, and `owners` can be omitted in submodules, in which + # case they will be inherited from their parent module. + if not self.includes and self.parent: + self.includes = self.parent.includes + + if not self.excludes and self.parent: + self.excludes = self.parent.excludes + + if not self.owners and self.parent: + self.owners = self.parent.owners + + def calculate_paths(self): + """Calculate paths based on inclusions and exclusions. + + Upon calling this method, excluded paths are parsed using `pathlib.Path.rglob` + and then subtracted from the included paths, which are parsed in the same way. + + :rtype: set + """ + includes = [] + for pattern in self.includes: + logger.debug(f"Expanding {pattern} in {self.machine_name}...") + expanded = list(self.repo_path.glob(pattern)) + logger.debug( + f"Pattern {pattern} expanded to {len(expanded)} included path(s)." + ) + includes += expanded + + excludes = [] + for pattern in self.excludes: + logger.debug(f"Expanding {pattern} in {self.machine_name}...") + expanded = list(self.repo_path.glob(pattern)) + logger.debug( + f"Pattern {pattern} expanded to {len(expanded)} excluded path(s)." + ) + excludes += expanded + + paths = set(includes) - set(excludes) + if self.exclude_submodule_paths: + for submodule in self.submodules: + paths -= submodule.calculate_paths() + return paths + + def validate(self, errors=None): + """Perform validation on module and submodules recursively. + + Starting with the current module, ensure that this module includes at least one + valid path and a valid name and machine name, and then run the same validation + on all submodules. + + :param errors: a list of errors to append to + :rtype: list + """ + errors = errors or [] + + if not self.machine_name.strip(): + errors.append("Module has a blank machine_name.") + + if " " in self.machine_name: + errors.append(f"Machine name {self.machine_name} contains white space.") + + if not self.calculate_paths(): + errors.append(f"No valid paths were found in {self.machine_name}.") + + # TODO validate owners + + if self.submodules: + for submodule in self.submodules: + return submodule.validate(errors=errors) + + return errors + + +def ls(modules: list[Module]): + """Print a list of given modules. + + :param modules: a list of :class:`Module` instances + """ + print(modules) + + +def show(modules: list[Module], module: str): + """Show details for a particular module. + + :param modules: a list of :class:`Module` instances + :param module: the `machine_name` of a given :class:`Module` + + :raises ValueError: when no module matches the given machine name + """ + modules_by_name = {} + for _module in modules: + modules_by_name[_module["machine_name"]] = _module + if module not in modules_by_name: + raise ValueError(f"{module} not found.") + else: + print(modules_by_name[module]) + + +def clean(file_config: FileConfig, write: bool = True): + """Clean and re-sort configuration file. + + Load condiguration from disk, sort modules and submodules by `machine_name`. If + there is no valid `machine_name`, generate one. Write changes to disk. + + :param file_config: an instance of :class:`FileConfig` + :param bool: if set to `True`, writes changes to disk. + """ + file_config.load() + file_config.config["modules"].sort(key=lambda x: x["machine_name"]) + for i in range(len(file_config.config["modules"])): + module = file_config.config["modules"][i] + if "machine_name" not in module: + module["machine_name"] = generate_machine_readable_name(module["name"]) + if "submodules" in module and module["submodules"]: + module["submodules"].sort(key=lambda x: x["name"]) + for submodule in module["submodules"]: + if "machine_name" not in submodule: + submodule["machine_name"] = generate_machine_readable_name( + submodule["name"] + ) + if write: + file_config.write() + + +def validate(config: dict, repo_path: str): + """Validate the current state of the config file. + + - Check if top-level dictionary contains required keys + - Check if machine names are unique + - Instantiate and run validation on each :class:`Module` instance + + :raises ValidationError: if any validation errors are detected + """ + # Validate that config has all the required keys. + required_keys = ("repo", "created_at", "updated_at", "modules") + keys_diff = required_keys - config.keys() + if len(keys_diff) != 0: + raise ValidationError(f"{keys_diff} missing from configuration file.") + + modules = config["modules"] + + # Count machine name repetitions in a defaultdict. + machine_names = defaultdict(int) + for m in modules: + machine_names[m["machine_name"]] += 1 + if "submodules" in m and m["submodules"]: + for sm in m["submodules"]: + machine_names[sm["machine_name"]] += 1 + + machine_names = {name: count for name, count in machine_names.items() if count > 1} + if machine_names: + raise ValidationError( + f"Duplicate machine name(s) found: {', '.join(machine_names.keys())}" + ) + + errors = [] + for i in range(len(config["modules"])): + module = config["modules"][i] + module = Module(repo_path=repo_path, **module) + validation_errors = module.validate() + if validation_errors: + errors.append(validation_errors) + + if errors: + raise ValidationError(errors) + logger.info("All modules validated successfully.") diff --git a/src/mots/utils.py b/src/mots/utils.py new file mode 100644 index 0000000..bf7b6bb --- /dev/null +++ b/src/mots/utils.py @@ -0,0 +1,12 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Utility helper functions.""" + + +def generate_machine_readable_name(display_name): + """Turn spaces into underscores, and lower the case. Strip all but alphanumerics.""" + words = [w.strip() for w in display_name.split(" ")] + alnum_words = ["".join([c.lower() for c in word if c.isalnum()]) for word in words] + return "_".join([w for w in alnum_words if w]) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..387a836 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,109 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Test configuration and fixtures.""" + + +import pytest +from mots.config import FileConfig + + +@pytest.fixture +def repo(tmp_path, config): + """Create a file structure in the file system and initialize a repo config. + + The below directory and file structure was created to provide different + opportunities for categorizing the paths within each directory. This was done in + the hope that it will make it easier to understand the different module definitions + in other fixtures and tests. + """ + test_repo = tmp_path / "test_repo" + test_repo.mkdir() + + [ + (test_repo / ".hg").mkdir(), + (test_repo / "canines").mkdir(), + (test_repo / "canines" / "chihuahuas").mkdir(), + (test_repo / "canines" / "chihuahuas" / "apple_head").touch(), + (test_repo / "canines" / "beagle").touch(), + (test_repo / "canines" / "corgy").touch(), + (test_repo / "canines" / "red_fox").touch(), + (test_repo / "canines" / "hyena").touch(), + (test_repo / "felines").mkdir(), + (test_repo / "felines" / "persian").touch(), + (test_repo / "felines" / "cheetah").touch(), + (test_repo / "bovines").mkdir(), + (test_repo / "bovines" / "cow").touch(), + (test_repo / "bovines" / "sheep").touch(), + (test_repo / "pigs").mkdir(), + (test_repo / "pigs" / "wild_boar").touch(), + (test_repo / "pigs" / "miniature_pig").touch(), + (test_repo / "marsupials").mkdir(), + (test_repo / "marsupials" / "kangaroo").touch(), + (test_repo / "marsupials" / "koala").touch(), + (test_repo / "birds").mkdir(), + (test_repo / "birds" / "parrot").touch(), + (test_repo / "birds" / "eagle").touch(), + ] + + file_config = FileConfig(test_repo / "mots.yml") + file_config.config = config + file_config.write() + + return test_repo + + +@pytest.fixture +def config(): + return { + "repo": "test_repo", + "created_at": "2021-09-10 12:53:22.383393", + "updated_at": "2021-09-10 12:53:22.383393", + "modules": [ + { + "machine_name": "domesticated_animals", + "exclude_submodule_paths": True, + "exclude_module_paths": True, + "includes": [ + "canines/**/*", + "felines/**/*", + "bovines/**/*", + "birds/**/*", + "pigs/**/*", + ], + "excludes": ["canines/red_fox"], + "owners": ["jane"], + "submodules": [ + { + "machine_name": "predators", + "includes": [ + "canines/hyena", + "felines/tiger", + "felines/cheetah", + "birds/**/*", + ], + "excludes": [ + "birds/parrot", + ], + "owners": ["jill"], + } + ], + }, + { + "machine_name": "pets", + "includes": [ + "canines/**/*", + "felines/**/*", + "birds/**/*", + ], + "excludes": [ + "canines/red_fox", + "canines/hyena", + "felines/cheetah", + "birds/eagle", + ], + "owners": ["otis"], + }, + ], + } diff --git a/tests/test_directory.py b/tests/test_directory.py new file mode 100644 index 0000000..3aa954a --- /dev/null +++ b/tests/test_directory.py @@ -0,0 +1,98 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Tests for directory module.""" + +from mots.directory import Directory +from mots.config import FileConfig + + +def test_directory__Directory(repo): + file_config = FileConfig(repo / "mots.yml") + directory = Directory(file_config) + directory.load(full_paths=True) + + rp = directory.repo_path + di = directory.index + + assert len(di) == 23 + + assert [m.machine_name for m in di[rp / "birds"]] == [] + assert [m.machine_name for m in di[rp / "birds/eagle"]] == ["predators"] + assert [m.machine_name for m in di[rp / "birds/parrot"]] == ["pets"] + assert [m.machine_name for m in di[rp / "bovines"]] == [] + assert [m.machine_name for m in di[rp / "bovines/cow"]] == ["domesticated_animals"] + assert [m.machine_name for m in di[rp / "bovines/sheep"]] == [ + "domesticated_animals" + ] + assert [m.machine_name for m in di[rp / "canines"]] == [] + assert [m.machine_name for m in di[rp / "canines/beagle"]] == ["pets"] + assert [m.machine_name for m in di[rp / "canines/chihuahuas"]] == ["pets"] + assert [m.machine_name for m in di[rp / "canines/chihuahuas/apple_head"]] == [ + "pets" + ] + assert [m.machine_name for m in di[rp / "canines/corgy"]] == ["pets"] + assert [m.machine_name for m in di[rp / "canines/hyena"]] == ["predators"] + assert [m.machine_name for m in di[rp / "canines/red_fox"]] == [] + assert [m.machine_name for m in di[rp / "felines"]] == [] + assert [m.machine_name for m in di[rp / "felines/cheetah"]] == ["predators"] + assert [m.machine_name for m in di[rp / "felines/persian"]] == ["pets"] + assert [m.machine_name for m in di[rp / "marsupials"]] == [] + assert [m.machine_name for m in di[rp / "marsupials/kangaroo"]] == [] + assert [m.machine_name for m in di[rp / "marsupials/koala"]] == [] + assert [m.machine_name for m in di[rp / "mots.yml"]] == [] + assert [m.machine_name for m in di[rp / "pigs"]] == [] + assert [m.machine_name for m in di[rp / "pigs/miniature_pig"]] == [ + "domesticated_animals" + ] + assert [m.machine_name for m in di[rp / "pigs/wild_boar"]] == [ + "domesticated_animals" + ] + + +def test_directory__Directory_new_path(repo): + file_config = FileConfig(repo / "mots.yml") + directory = Directory(file_config) + directory.load() + new_file_path = directory.repo_path / "canines/chihuahuas/deer_head" + new_file_path.touch() + assert new_file_path not in directory.index + directory.load() + assert new_file_path in directory.index + assert len(directory.index[new_file_path]) == 1 + assert directory.index[new_file_path][0].machine_name == "pets" + + +def test_directory__Directory_deleted_path(repo): + file_config = FileConfig(repo / "mots.yml") + directory = Directory(file_config) + directory.load() + existing_path = directory.repo_path / "canines/chihuahuas/apple_head" + assert len(directory.index[existing_path]) == 1 + assert directory.index[existing_path][0].machine_name == "pets" + assert existing_path in directory.index + existing_path.unlink() + directory.load() + assert existing_path not in directory.index + + +def test_directory__Directory__query(repo): + file_config = FileConfig(repo / "mots.yml") + directory = Directory(file_config) + directory.load() + + paths_to_check = [ + "canines/chihuahuas/apple_head", + "birds/parrot", + "felines/persian", + "felines/maine_coon", + ] + + result, rejected = directory.query(*paths_to_check) + assert result == { + "canines/chihuahuas/apple_head": [directory.modules_by_machine_name["pets"]], + "birds/parrot": [directory.modules_by_machine_name["pets"]], + "felines/persian": [directory.modules_by_machine_name["pets"]], + } + assert rejected == ["felines/maine_coon"] diff --git a/tests/test_module.py b/tests/test_module.py new file mode 100644 index 0000000..3afbf89 --- /dev/null +++ b/tests/test_module.py @@ -0,0 +1,137 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Integration tests for mots.module.""" + +from __future__ import annotations +import pytest + +from mots.config import FileConfig +from mots.module import Module +from mots.module import ValidationError +from mots.module import validate + + +def test_module__Module(repo): + file_config = FileConfig(repo / "mots.yml") + file_config.load() + modules = file_config.config["modules"] + + assert len(modules) == 2 + m = Module(repo_path=repo, **modules[0]) + + assert m.machine_name == "domesticated_animals" + assert len(m.includes) == 5 + assert len(m.excludes) == 1 + assert len(m.submodules) == 1 + assert len(m.owners) == 1 + + assert m.submodules[0].machine_name == "predators" + assert len(m.submodules[0].includes) == 4 + # TODO: should excludes be inherited like this? Might need to revisit this. + assert len(m.submodules[0].excludes) == 1 + assert len(m.submodules[0].owners) == 1 + + +def test_module__Module__calculate_paths(repo): + file_config = FileConfig(repo / "mots.yml") + file_config.load() + modules = file_config.config["modules"] + m = Module(repo_path=repo, **modules[0]) + assert len(m.calculate_paths()) == 10 + assert len(m.submodules[0].calculate_paths()) == 3 + + +def test_module__Module__validate(repo): + m = Module( + name="Some Module", + machine_name="some_module", + repo_path=str(repo), + includes="*", + ) + errors = m.validate() + assert errors == [] + + +def test_module__Module__validate__error_no_paths_in_submodule(repo): + """Ensure an error is thrown when a submodule contains no valid paths.""" + m = dict( + name="Some Module", + machine_name="some_module", + includes="*", + submodules=[ + dict( + name="Submodule", + machine_name="submodule", + excludes="*", + ) + ], + ) + m = Module(**m, repo_path=str(repo)) + errors = m.validate() + assert errors == ["No valid paths were found in submodule."] + + +def test_module__Module__validate__invalid_machine_name(repo): + """Ensure an error is thrown when a submodule has an invalid machine name.""" + m = dict( + machine_name="some module", + includes="*", + ) + m = Module(**m, repo_path=str(repo)) + errors = m.validate() + assert errors == ["Machine name some module contains white space."] + + m = dict( + machine_name="", + includes="*", + ) + m = Module(**m, repo_path=str(repo)) + errors = m.validate() + assert errors == ["Module has a blank machine_name."] + + +def test_module__validate(repo, config): + validate(config, str(repo)) + + +def test_module__validate__error_duplicate_machine_names(repo): + """Ensure an error is thrown when there are duplicate machine names in modules.""" + config = { + "repo": "test_repo", + "created_at": "2021-09-10 12:53:22.383393", + "updated_at": "2021-09-10 12:53:22.383393", + "modules": [ + { + "name": "m1", + "machine_name": "m1", + "includes": ["*"], + "submodules": [ + { + "name": "m1", + "machine_name": "m1", + }, + { + "name": "m3", + "machine_name": "m3", + }, + ], + }, + { + "name": "m2", + "machine_name": "m2", + "includes": ["*"], + "submodules": [ + { + "name": "m3", + "machine_name": "m3", + }, + ], + }, + ], + } + with pytest.raises(ValidationError) as e: + validate(config, repo) + + assert e.value.args[0] == "Duplicate machine name(s) found: m1, m3" diff --git a/tests/test_style.py b/tests/test_style.py new file mode 100644 index 0000000..edfc0d5 --- /dev/null +++ b/tests/test_style.py @@ -0,0 +1,22 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Python code style tests.""" + +import subprocess + + +def test_check_black(): + """Check that black runs cleanly and that no files require reformatting.""" + cmd = ("black", "--diff", "src/mots") + output = subprocess.check_output(cmd) + assert not output + + +def test_check_flake8(): + """Check that flake8 runs cleanly and does not produce any errors.""" + cmd = ("flake8", "--exit-zero", "tests", "src") + output = subprocess.check_output(cmd) + if output: + raise AssertionError(output.decode("utf-8")) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..dcd1bc4 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,15 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Tests for utils module.""" + +from mots.utils import generate_machine_readable_name + + +def test_generate_machine_readable_name(): + assert generate_machine_readable_name("Hello, world!") == "hello_world" + assert generate_machine_readable_name("test") == "test" + assert generate_machine_readable_name("Another test !@%#@") == "another_test" + assert generate_machine_readable_name("User Interface (UI)") == "user_interface_ui" + assert generate_machine_readable_name("test: testing") == "test_testing" From eeb823c51fbf75c2acc46aa120e8dc27e2d51e6c Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Mon, 18 Oct 2021 13:45:50 -0400 Subject: [PATCH 02/26] Additional improvements (to be squashed with previous commit) NOTE: could also break this into different branches and facilitate code review that way. Includes: - PMO and BMO thin clients that were used to parse and validate users - bug fix to return rejected paths when querying - cli command to add module (already obsolete?) and helper function - bug fix when determining repo path in config (was fetching bad index) - Directory class improvements, including: - new People and Person helper classes - load people into Directory.people - serialize people and rewrite yaml when needed - validate people against BMO user IDs - improvement to yaml config format including using named anchors - improved logging - helper function to parse user string (used in import, but reusable) - fixed serialization of modules - fixed references to people in yaml (uses nick-named anchors now) TODO: - improve PMO/BMO client implementation - clean up helper functions - refactor/clean up module.py to have less repetition in submodules - move `extract_people` to utils - add more tests and mock BMO client --- .gitignore | 6 +++ src/mots/bmo.py | 106 ++++++++++++++++++++++++++++++++++++ src/mots/cli.py | 26 ++++++++- src/mots/config.py | 3 +- src/mots/directory.py | 63 +++++++++++++++++++++- src/mots/logging.py | 4 +- src/mots/module.py | 116 +++++++++++++++++++++++++++++++++++++++- src/mots/pmo.py | 37 +++++++++++++ src/mots/utils.py | 40 +++++++++++++- tests/conftest.py | 12 +++-- tests/test_directory.py | 12 ++--- tests/test_module.py | 34 ++++++++++++ tests/test_utils.py | 37 ++++++++++++- 13 files changed, 478 insertions(+), 18 deletions(-) create mode 100644 src/mots/bmo.py create mode 100644 src/mots/pmo.py diff --git a/.gitignore b/.gitignore index 852da0f..58595cf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ documentation/_build htmlcov/ mots.log notes +# should be removed later +dump +parse_wiki.py +wiki +wiki.yaml +src/mots/parking_lot.py diff --git a/src/mots/bmo.py b/src/mots/bmo.py new file mode 100644 index 0000000..3cf0c9e --- /dev/null +++ b/src/mots/bmo.py @@ -0,0 +1,106 @@ +"""Module that provides helpers to interact with the Bugzilla API.""" + +# TODO: this should be abstracted so that it can be hot-swappable with any API that can +# be searched. + +from __future__ import annotations +import requests +import os +import logging +import time + + +logger = logging.getLogger(__name__) + +DEFAULT_BASE_URL = "https://bugzilla.mozilla.org/rest" +USER_AGENT = "mots" # TODO: improve this and include version. + +# TODO set NO_INPUT var to prevent prompt. + + +class BMOClient: + """A thin wrapper as a Bugzilla API client.""" + + def __init__(self, token: str = None, base_url: str = DEFAULT_BASE_URL): + if not token: + token = os.getenv("BUGZILLA_API_KEY", input("Enter BMO Token: ")) + if not token: + raise Exception() + self.headers = {"X-BUGZILLA-API-KEY": token, "User-Agent": USER_AGENT} + self.base_url = base_url + + def _get(self, path: str, params=None): + logger.debug(f"GET {self.base_url}/{path}") + params = params or {} + response = requests.get( + f"{self.base_url}/{path}", headers=self.headers, params=params + ) + return response + + def get_user(self, email: str, rate_limit_delay: float = 0.3): + """Get user data based on provided email address.""" + time.sleep(rate_limit_delay) + fields = ["real_name", "nick", "email", "name", "id"] + response = self._get( + "user", {"names": email, "include_fields": ",".join(fields)} + ) + if response.status_code != 200: + logger.error(f"Error searching {email}: {response.status_code}") + return + + # TODO: handle case when there is more than one match. + # Since possibly users can sneak in their email address in another field? + # Although we should validate the "email" field in BMO response + check for + # verification. + + result = response.json() + return result["users"][0] + + def get_user_by_id(self, id_: str = "", rate_limit_delay: float = 0.3): + """Get user data by BMO ID.""" + time.sleep(rate_limit_delay) + fields = ["real_name", "nick", "name", "id"] + response = self._get("user", {"ids": id_, "include_fields": ",".join(fields)}) + if response.status_code != 200: + logger.error(f"Error searching {id_}: {response.status_code}") + return + + result = response.json() + return result["users"][0] + + def get_users_by_ids(self, ids: list[str], rate_limit_delay: float = 0.3): + """Get user data by BMO ID.""" + time.sleep(rate_limit_delay) + fields = ["real_name", "nick", "name", "id", "email"] + response = self._get("user", {"ids": ids, "include_fields": ",".join(fields)}) + if response.status_code != 200: + logger.error(f"Error searching {ids}: {response.status_code}") + return + + result = response.json() + return {u["id"]: u for u in result["users"]} + + def get_match(self, match: str, rate_limit_delay: float = 0.3): + """Get user data based on provided info.""" + time.sleep(rate_limit_delay) + fields = ["real_name", "nick", "email", "name", "id"] + response = self._get( + "user", {"match": match, "include_fields": ",".join(fields)} + ) + if response.status_code != 200: + logger.error(f"Error searching {match}: {response.status_code}") + return + + result = response.json() + if result["users"]: + return result["users"][0] + else: + return None + return result["users"][0] + + # NOTES: + # How to store people? + # 64523: glob -> if nick is available + # 65182: Jane Smith -> if nick is not available, use provided name. + # OR + # - Masayuki Nakano bmo:19563 diff --git a/src/mots/cli.py b/src/mots/cli.py index 393cc78..283001e 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -31,6 +31,8 @@ from mots.logging import init_logging from mots.config import DEFAULT_CONFIG_FILEPATH +from mots.utils import get_list_input + logger = logging.getLogger(__name__) @@ -78,13 +80,32 @@ def query(args): file_config.load() directory = Directory(file_config) directory.load() - result = directory.query(*args.paths) + result, rejected = directory.query(*args.paths) for path in result: modules = result[path] module_names = ",".join([m.machine_name for m in modules]) owners = ",".join([",".join(m.owners) for m in modules]) sys.stdout.write(f"{path}:{module_names}:{owners}\n") + @staticmethod + def add(args): + """Prompt user to add a new module.""" + params = { + "machine_name": input("Enter machine name of new module: "), + "name": input("Enter a human readable name: "), + "owners": get_list_input("Enter a comma separated list of owners"), + "peers": get_list_input("Enter a comma separated list of owners"), + "includes": get_list_input( + "Enter a comma separated list of paths to include" + ), + "excludes": get_list_input( + "Enter a comma separated list of paths to exclude" + ), + } + parent = input("Enter a machine name of the parent module (optional): ") or None + file_config = FileConfig(Path(args.path)) + module.add(params, file_config, parent=parent, write=True) + def main(): """Redirect to appropriate function.""" @@ -133,6 +154,9 @@ def create_parser(): list_parser = module_parsers.add_parser("list", help="list all modules") list_parser.set_defaults(func=CLI.ls) + add_parser = module_parsers.add_parser("add", help="add a new module") + add_parser.set_defaults(func=CLI.add) + show_parser = module_parsers.add_parser("show", help="show a module") show_parser.add_argument("module", help="name of the module to show") show_parser.set_defaults(func=CLI.show) diff --git a/src/mots/config.py b/src/mots/config.py index 9ccb7b8..f97f7d7 100644 --- a/src/mots/config.py +++ b/src/mots/config.py @@ -12,6 +12,7 @@ logger = logging.getLogger(__name__) yaml = YAML() +yaml.indent(mapping=2, sequence=4, offset=2) DEFAULT_CONFIG_FILEPATH = "./mots.yaml" @@ -33,7 +34,7 @@ def init(self): # File does not exist, create it. now = datetime.now() self.config = { - "repo": str(Path(self.path).resolve().parts[-1]), + "repo": str(Path(self.path).resolve().parts[-2]), "created_at": now, "updated_at": None, "modules": None, diff --git a/src/mots/directory.py b/src/mots/directory.py index f47f2b0..c89840c 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -6,7 +6,11 @@ from __future__ import annotations from collections import defaultdict +from dataclasses import asdict +from dataclasses import dataclass +from dataclasses import InitVar import logging +from mots.bmo import BMOClient from mots.module import Module from mots.config import FileConfig @@ -31,8 +35,9 @@ def __init__(self, config: FileConfig): self.modules_by_machine_name[module.machine_name] = module self.index = None + self.people = None - def load(self, full_paths: bool = False): + def load(self, full_paths: bool = False, query_bmo=True): """Load all paths in each module and put them in index.""" self.index = defaultdict(list) @@ -64,6 +69,12 @@ def load(self, full_paths: bool = False): self.index[path] = [m for m in modules if not m.exclude_module_paths] self.index = dict(self.index) + # Load people directory + self.people = People(self.config_handle.config["people"], query_bmo=query_bmo) + if self.people.serialized != list(self.config_handle.config["people"]): + logger.debug("People directory modified, updating configuration...") + self.config_handle.config["people"] = self.people.serialized + def query(self, *paths: str) -> tuple[list[Module], list[str]]: """Query given paths and return a list of corresponding modules.""" result = {} @@ -79,3 +90,53 @@ def query(self, *paths: str) -> tuple[list[Module], list[str]]: # NOTE: for new files, we need to use the old config against the new file # structure. return result, rejected + + +@dataclass +class Person: + """A class representing a person.""" + + name: str = "" + bmo_id: int = None + real_name: str = "" + nick: str = "" + bmo_data: InitVar[dict] = None + + def __post_init__(self, bmo_data): + """Refresh BMO data from BMO API.""" + if bmo_data: + self.nick = bmo_data.get("nick", "") + self.real_name = bmo_data.get("real_name", "") + + +class People: + """A people directory searchable by name, email, or BMO ID.""" + + by_bmo_id: dict = None + people: list = None + serialized: list = None + + def __init__(self, people, query_bmo: bool = True): + logger.debug(f"Initializing people directory with {len(people)} people...") + if query_bmo: + bmo_client = BMOClient() + bmo_data = bmo_client.get_users_by_ids([p["bmo_id"] for p in people]) + else: + bmo_data = {} + people = list(people) + self.people = [] + self.by_bmo_id = {} + for i in range(len(people)): + p = people[i] + logger.debug(f"Adding person {p} to roster...") + self.people.append( + Person( + name=p["name"], + real_name=p["real_name"], + bmo_id=p["bmo_id"], + bmo_data=bmo_data.get(p["bmo_id"]), + ) + ) + self.by_bmo_id[p["bmo_id"]] = i + logger.debug(f"Person {p} added to position {i}.") + self.serialized = [asdict(p) for p in self.people] diff --git a/src/mots/logging.py b/src/mots/logging.py index 67edeeb..87cde39 100644 --- a/src/mots/logging.py +++ b/src/mots/logging.py @@ -33,6 +33,7 @@ def init_logging( "console": { "class": "logging.StreamHandler", "formatter": "standard", + "level": logging.DEBUG if debug else logging.WARNING, }, "file": { "class": "logging.handlers.RotatingFileHandler", @@ -41,12 +42,13 @@ def init_logging( "maxBytes": log_max_size, "backupCount": log_backups, "encoding": "utf8", + "level": logging.DEBUG, }, }, "loggers": { "mots": { - "level": logging.DEBUG if debug else logging.WARNING, "handlers": ["console", "file"], + "level": logging.DEBUG, }, }, } diff --git a/src/mots/module.py b/src/mots/module.py index 6e78388..39d6411 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -26,9 +26,11 @@ class Module: :param machine_name: a unique, machine-readable name for the module :param repo_path: the path of the top-level repository :param name: the name of the module + :param description: the description of the module :param includes: a list of paths (glob format) to include :param excludes: a list of paths (glob format) to exclude :param owners: a list of owners that will own all paths in this module + :param peers: a list of peers for this module :param meta: a dictionary of meta data related to this module :param parent: the parent module of this module, if this is a submodule :param submodules: a list of submodules of this module @@ -46,9 +48,11 @@ def __init__( machine_name: str, repo_path: str, name: str = None, + description: str = None, includes: str = None, excludes: str = None, owners: list[str] = None, + peers: list[str] = None, meta: dict = None, parent: "Module" = None, submodules: list[dict] = None, @@ -63,6 +67,7 @@ def __init__( self.excludes = excludes or [] self.includes = includes or [] self.owners = owners or [] + self.peers = owners or [] self.submodules = [] self.exclude_submodule_paths = exclude_submodule_paths self.exclude_module_paths = exclude_module_paths @@ -87,6 +92,9 @@ def __init__( if not self.owners and self.parent: self.owners = self.parent.owners + if not self.peers and self.parent: + self.peers = self.parent.peers + def calculate_paths(self): """Calculate paths based on inclusions and exclusions. @@ -119,6 +127,20 @@ def calculate_paths(self): paths -= submodule.calculate_paths() return paths + def serialize(self): + """Return a dictionary with relevant module information.""" + serialized = { + "machine_name": self.machine_name, + "name": self.name, + "includes": self.includes, + "excludes": self.excludes, + "owners": self.owners, + "peers": self.peers, + "meta": self.meta, + } + + return serialized + def validate(self, errors=None): """Perform validation on module and submodules recursively. @@ -140,7 +162,19 @@ def validate(self, errors=None): if not self.calculate_paths(): errors.append(f"No valid paths were found in {self.machine_name}.") - # TODO validate owners + # # TODO do this validation against BMO/mock client + # if self.owners: + # for owner in self.owners: + # logger.debug(f"Parsing owner {owner}...") + # owner = parse_user_string(owner) + # if not owner["email"]: + # errors.append(f"No valid email found for owner {owner['name']}") + # if self.peers: + # for peer in self.peers: + # logger.debug(f"Parsing peer {peer}...") + # peer = parse_user_string(peer) + # if not peer["email"]: + # errors.append(f"No valid email found for peer {peer['name']}") if self.submodules: for submodule in self.submodules: @@ -174,6 +208,17 @@ def show(modules: list[Module], module: str): print(modules_by_name[module]) +def extract_people(module): + """Return a list of people that are in a module or submodule.""" + people_keys = ["owners", "peers"] # "owners_emeritus", "peers_emeritus" + people = [] + for key in people_keys: + if key in module and module[key]: + logger.debug(f"Extracting people from {module[key]} ({key})") + people += module[key] + return people + + def clean(file_config: FileConfig, write: bool = True): """Clean and re-sort configuration file. @@ -183,19 +228,58 @@ def clean(file_config: FileConfig, write: bool = True): :param file_config: an instance of :class:`FileConfig` :param bool: if set to `True`, writes changes to disk. """ + from mots.directory import Directory + file_config.load() - file_config.config["modules"].sort(key=lambda x: x["machine_name"]) + directory = Directory(file_config) + directory.load() for i in range(len(file_config.config["modules"])): module = file_config.config["modules"][i] if "machine_name" not in module: module["machine_name"] = generate_machine_readable_name(module["name"]) + + people_keys = ("owners", "peers") + for key in people_keys: + if key in module and module[key]: + for i in range(len(module[key])): + person = module[key][i] + module[key][i] = file_config.config["people"][ + directory.people.by_bmo_id[person["bmo_id"]] + ] + + # Do the same for submodules. if "submodules" in module and module["submodules"]: module["submodules"].sort(key=lambda x: x["name"]) for submodule in module["submodules"]: + for key in people_keys: + if key in submodule and submodule[key]: + for i in range(len(submodule[key])): + person = submodule[key][i] + submodule[key][i] = file_config.config["people"][ + directory.people.by_bmo_id[person["bmo_id"]] + ] if "machine_name" not in submodule: submodule["machine_name"] = generate_machine_readable_name( submodule["name"] ) + + file_config.config["modules"].sort(key=lambda x: x["machine_name"]) + + nicks = [] + for p in file_config.config["people"]: + machine_readable_nick = generate_machine_readable_name( + p["nick"], keep_case=True + ) + if machine_readable_nick in nicks or not machine_readable_nick: + continue + nicks.append(machine_readable_nick) + try: + p.yaml_set_anchor(machine_readable_nick) + except Exception as e: + # NOTE: this happens when the file changes and we lose ruamel embedded data + # TODO: fix this + logger.error(e) + if write: file_config.write() @@ -242,3 +326,31 @@ def validate(config: dict, repo_path: str): if errors: raise ValidationError(errors) logger.info("All modules validated successfully.") + + +def add( + new_module: dict, file_config: FileConfig, parent: str = None, write: bool = True +): + """Add a new module to the configuration. + + :param module: a dictionary containing module parameters + :param file_config: an instance of :class:`FileConfig` + :param parent: the machine name of the parent module if applicable + :param bool: if set to `True`, writes changes to disk + """ + file_config.load() + modules = file_config.config["modules"] + serialized = Module(**new_module, repo_path=file_config.repo_path).serialize() + + if parent: + for module in modules: + if module["machine_name"] == parent: + if "submodules" not in module or not module["submodules"]: + module["submodules"] = [] + module["submodules"].append(serialized) + break + else: + modules.append(serialized) + + if write: + file_config.write() diff --git a/src/mots/pmo.py b/src/mots/pmo.py new file mode 100644 index 0000000..9a8c181 --- /dev/null +++ b/src/mots/pmo.py @@ -0,0 +1,37 @@ +"""Module that provides helpers to interact with the PMO API.""" + +import requests +import os +import logging + + +logger = logging.getLogger(__name__) + +DEFAULT_BASE_URL = "https://people.mozilla.org/api/v4/" +USER_AGENT = "mots" # TODO: improve this and include version. + + +class PMOClient: + """A thin wrapper as a PMO API client.""" + + def __init__(self, token: str = None, base_url: str = DEFAULT_BASE_URL): + if not token: + token = os.getenv("PMO_COOKIE", input("Enter PMO Cookie: ")) + if not token: + raise Exception() + self.headers = {"Cookie": token, "User-Agent": USER_AGENT} + self.base_url = base_url + + def _get(self, path: str, params=None): + params = params or {} + response = requests.get( + f"{self.base_url}/{path}", headers=self.headers, params=params + ) + return response + + def search(self, email: str): + """Search PMO for a particular email.""" + params = {"q": email, "w": "all"} + path = "search/simple/" + + return self._get(path, params).json() diff --git a/src/mots/utils.py b/src/mots/utils.py index bf7b6bb..5ee6495 100644 --- a/src/mots/utils.py +++ b/src/mots/utils.py @@ -4,9 +4,45 @@ """Utility helper functions.""" +import logging +import re -def generate_machine_readable_name(display_name): +logger = logging.getLogger(__name__) + + +def generate_machine_readable_name(display_name, keep_case=False): """Turn spaces into underscores, and lower the case. Strip all but alphanumerics.""" words = [w.strip() for w in display_name.split(" ")] - alnum_words = ["".join([c.lower() for c in word if c.isalnum()]) for word in words] + if keep_case: + alnum_words = ["".join([c for c in word if c.isalnum()]) for word in words] + else: + alnum_words = [ + "".join([c.lower() for c in word if c.isalnum()]) for word in words + ] return "_".join([w for w in alnum_words if w]) + + +def get_list_input(text: str): + """Parse comma separated list in user input into a list. + + :param test: the text to prompt the user with + """ + return [o.strip() for o in input(f"{text}: ").split(",") if o] + + +def parse_user_string(string): + """Return user data based on provided string. + + Example: + >>> test = parse_user_string("charlie jones (cj) ") + >>> print(test) + >>> {"meta": "cj", "email": "cjones@example.org", "name": "charlie jones"} + """ + logger.debug(f"Parsing provided string {string}...") + pattern = re.compile( + r"^((?P([\w\-'.]+)( +[\w\-'.]+)*)\s?)" + r"(\((?P.*)\))?\s?(\<(?P.*)\>)?$" + ) + match = pattern.match(string) + if match: + return match.groupdict() diff --git a/tests/conftest.py b/tests/conftest.py index 387a836..6e32d68 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,10 +56,16 @@ def repo(tmp_path, config): @pytest.fixture def config(): + people = [ + {"real_name": "jane", "nick": "jane", "name": "jane", "bmo_id": 0}, + {"real_name": "jill", "nick": "jill", "name": "jill", "bmo_id": 1}, + {"real_name": "otis", "nick": "otis", "name": "otis", "bmo_id": 2}, + ] return { "repo": "test_repo", "created_at": "2021-09-10 12:53:22.383393", "updated_at": "2021-09-10 12:53:22.383393", + "people": people, "modules": [ { "machine_name": "domesticated_animals", @@ -73,7 +79,7 @@ def config(): "pigs/**/*", ], "excludes": ["canines/red_fox"], - "owners": ["jane"], + "owners": [people[0]], "submodules": [ { "machine_name": "predators", @@ -86,7 +92,7 @@ def config(): "excludes": [ "birds/parrot", ], - "owners": ["jill"], + "owners": [people[1]], } ], }, @@ -103,7 +109,7 @@ def config(): "felines/cheetah", "birds/eagle", ], - "owners": ["otis"], + "owners": [people[2]], }, ], } diff --git a/tests/test_directory.py b/tests/test_directory.py index 3aa954a..d80ab65 100644 --- a/tests/test_directory.py +++ b/tests/test_directory.py @@ -11,7 +11,7 @@ def test_directory__Directory(repo): file_config = FileConfig(repo / "mots.yml") directory = Directory(file_config) - directory.load(full_paths=True) + directory.load(full_paths=True, query_bmo=False) rp = directory.repo_path di = directory.index @@ -54,11 +54,11 @@ def test_directory__Directory(repo): def test_directory__Directory_new_path(repo): file_config = FileConfig(repo / "mots.yml") directory = Directory(file_config) - directory.load() + directory.load(query_bmo=False) new_file_path = directory.repo_path / "canines/chihuahuas/deer_head" new_file_path.touch() assert new_file_path not in directory.index - directory.load() + directory.load(query_bmo=False) assert new_file_path in directory.index assert len(directory.index[new_file_path]) == 1 assert directory.index[new_file_path][0].machine_name == "pets" @@ -67,20 +67,20 @@ def test_directory__Directory_new_path(repo): def test_directory__Directory_deleted_path(repo): file_config = FileConfig(repo / "mots.yml") directory = Directory(file_config) - directory.load() + directory.load(query_bmo=False) existing_path = directory.repo_path / "canines/chihuahuas/apple_head" assert len(directory.index[existing_path]) == 1 assert directory.index[existing_path][0].machine_name == "pets" assert existing_path in directory.index existing_path.unlink() - directory.load() + directory.load(query_bmo=False) assert existing_path not in directory.index def test_directory__Directory__query(repo): file_config = FileConfig(repo / "mots.yml") directory = Directory(file_config) - directory.load() + directory.load(query_bmo=False) paths_to_check = [ "canines/chihuahuas/apple_head", diff --git a/tests/test_module.py b/tests/test_module.py index 3afbf89..2483c2f 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -8,6 +8,7 @@ import pytest from mots.config import FileConfig +from mots.module import add from mots.module import Module from mots.module import ValidationError from mots.module import validate @@ -92,6 +93,10 @@ def test_module__Module__validate__invalid_machine_name(repo): assert errors == ["Module has a blank machine_name."] +def test_module__Module__serialize(repo): + assert True + + def test_module__validate(repo, config): validate(config, str(repo)) @@ -135,3 +140,32 @@ def test_module__validate__error_duplicate_machine_names(repo): validate(config, repo) assert e.value.args[0] == "Duplicate machine name(s) found: m1, m3" + + +def test_module__add(repo): + file_config = FileConfig(repo / "mots.yml") + file_config.load() + + module = { + "machine_name": "reptiles", + "includes": ["reptiles/**/*"], + "owners": ["angel"], + } + + add(module, file_config, parent="pets", write=True) + file_config.load() + # NOTE should we be storing machine names as keys in a dict, instead of a list? + # even an ordered dict? This might make some things a little easier. + assert ( + file_config.config["modules"][1]["submodules"][0]["machine_name"] == "reptiles" + ) + assert file_config.config["modules"][1]["submodules"][0]["includes"] == [ + "reptiles/**/*" + ] + assert file_config.config["modules"][1]["submodules"][0]["owners"] == ["angel"] + + # TODO: validate that includes, excludes, and owners are lists... + + +def test_module__clean(repo): + pass diff --git a/tests/test_utils.py b/tests/test_utils.py index dcd1bc4..c457cca 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,7 +4,7 @@ """Tests for utils module.""" -from mots.utils import generate_machine_readable_name +from mots.utils import generate_machine_readable_name, parse_user_string def test_generate_machine_readable_name(): @@ -13,3 +13,38 @@ def test_generate_machine_readable_name(): assert generate_machine_readable_name("Another test !@%#@") == "another_test" assert generate_machine_readable_name("User Interface (UI)") == "user_interface_ui" assert generate_machine_readable_name("test: testing") == "test_testing" + + +def test_parse_user_string(): + test = parse_user_string("Robin Smith (robin) ") + assert test == { + "name": "Robin Smith", + "meta": "robin", + "email": "robin@mozilla.com", + } + + test = parse_user_string("Robin Smith ") + assert test == {"name": "Robin Smith", "meta": None, "email": "robin@mozilla.com"} + + test = parse_user_string("Robin Smith") + assert test == {"name": "Robin Smith", "meta": None, "email": None} + + test = parse_user_string("robin ") + assert test == {"name": "robin", "meta": None, "email": "robin@mozilla.com"} + + test = parse_user_string("robin middle smith ") + assert test == { + "name": "robin middle smith", + "meta": None, + "email": "robin@mozilla.com", + } + + test = parse_user_string("r. middle double-o'smith (meta) ") + assert test == { + "name": "r. middle double-o'smith", + "meta": "meta", + "email": "robin@mozilla.com", + } + + test = parse_user_string("") + assert test is None From 3ebc629ce84c305f674d21d6045a5a0c1721768d Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Mon, 8 Nov 2021 13:03:10 -0500 Subject: [PATCH 03/26] circleci: add circleci configuration (bug-1740083) --- .circleci/config.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..7c5cd68 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,36 @@ +version: 2.1 + +workflows: + version: 2 + test: + jobs: + - test_3_9 + +jobs: + test_3_9: + docker: + - image: circleci/python:3.9 + steps: + - checkout + - run: + name: setup + command: | + set -e + sudo apt-get install python3-venv + mkdir -p /tmp/test-reports + make dev-env PY=python3.9 + source .mots-env/bin/activate + make dev + - run: + name: run tests + command: | + set -e + source .mots-env/bin/activate + ulimit -c unlimited + for F in tests/test_*.py; do + pytest --junitxml=/tmp/test-reports/junit-$( basename $F .py ).xml -vv $F + done + - store_test_results: + path: /tmp/test-reports + - store_artifacts: + path: /tmo/test-reports From 645fb9a2dc236f94a7badb5e48b0f05990c7ba45 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Mon, 29 Nov 2021 12:41:56 -0500 Subject: [PATCH 04/26] Additional improvements - fix bug with BMO token input - fix bug with `module add` command --- src/mots/bmo.py | 4 +++- src/mots/cli.py | 2 +- src/mots/config.py | 1 + src/mots/directory.py | 2 -- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mots/bmo.py b/src/mots/bmo.py index 3cf0c9e..18c96ca 100644 --- a/src/mots/bmo.py +++ b/src/mots/bmo.py @@ -23,7 +23,9 @@ class BMOClient: def __init__(self, token: str = None, base_url: str = DEFAULT_BASE_URL): if not token: - token = os.getenv("BUGZILLA_API_KEY", input("Enter BMO Token: ")) + token = os.getenv("BUGZILLA_API_KEY", "") + if not token: + token = input("Enter BMO Token: ") if not token: raise Exception() self.headers = {"X-BUGZILLA-API-KEY": token, "User-Agent": USER_AGENT} diff --git a/src/mots/cli.py b/src/mots/cli.py index 283001e..1ea35f7 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -94,7 +94,7 @@ def add(args): "machine_name": input("Enter machine name of new module: "), "name": input("Enter a human readable name: "), "owners": get_list_input("Enter a comma separated list of owners"), - "peers": get_list_input("Enter a comma separated list of owners"), + "peers": get_list_input("Enter a comma separated list of peers"), "includes": get_list_input( "Enter a comma separated list of paths to include" ), diff --git a/src/mots/config.py b/src/mots/config.py index f97f7d7..c6c6c8e 100644 --- a/src/mots/config.py +++ b/src/mots/config.py @@ -13,6 +13,7 @@ yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) +yaml.default_style = '""' DEFAULT_CONFIG_FILEPATH = "./mots.yaml" diff --git a/src/mots/directory.py b/src/mots/directory.py index c89840c..0beaf33 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -96,7 +96,6 @@ def query(self, *paths: str) -> tuple[list[Module], list[str]]: class Person: """A class representing a person.""" - name: str = "" bmo_id: int = None real_name: str = "" nick: str = "" @@ -131,7 +130,6 @@ def __init__(self, people, query_bmo: bool = True): logger.debug(f"Adding person {p} to roster...") self.people.append( Person( - name=p["name"], real_name=p["real_name"], bmo_id=p["bmo_id"], bmo_data=bmo_data.get(p["bmo_id"]), From 8e0165d1e48db46a99bf4ca679452bdd37fc34c1 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Tue, 30 Nov 2021 13:54:16 -0500 Subject: [PATCH 05/26] Additional improvements - add readthedocs config - update directory query response to use QueryResponse class - conftest bug fix --- .readthedocs.yaml | 13 +++++++++ src/mots/directory.py | 58 +++++++++++++++++++++++++++++++++++++---- tests/conftest.py | 6 ++--- tests/test_directory.py | 14 +++++++--- 4 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3192b22 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "3.9" + +sphinx: + configuration: documentation/conf.py + +python: + install: + - requirements: requirements.txt diff --git a/src/mots/directory.py b/src/mots/directory.py index 0beaf33..2db904c 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -75,7 +75,7 @@ def load(self, full_paths: bool = False, query_bmo=True): logger.debug("People directory modified, updating configuration...") self.config_handle.config["people"] = self.people.serialized - def query(self, *paths: str) -> tuple[list[Module], list[str]]: + def query(self, *paths: str) -> QueryResult: """Query given paths and return a list of corresponding modules.""" result = {} rejected = [] @@ -85,11 +85,54 @@ def query(self, *paths: str) -> tuple[list[Module], list[str]]: rejected.append(path) continue result[path] = self.index.get(self.repo_path / path, list()) - logger.info(f"Query {paths} resolved to {result}.") + logger.debug(f"Query {paths} resolved to {result}.") - # NOTE: for new files, we need to use the old config against the new file - # structure. - return result, rejected + return QueryResult(result, rejected) + + +class QueryResult: + """Helper class to simplify query result interpretation.""" + + paths: list = None + path_map: dict = None + rejected_paths: list = None + modules: list = None + owners: list = None + peers: list = None + + data_keys = { + "paths", + "rejected_paths", + "modules", + "owners", + "peers", + } + + def __init__(self, result, rejected): + data = {k: list() for k in self.data_keys} + self.path_map = result + + for path in self.path_map: + data["paths"].append(path) + data["modules"] += self.path_map[path] + + for module in data["modules"]: + # TODO: this conversion should happen elsewhere. + data["owners"] += [Person(**o) for o in module.owners] + data["peers"] += [Person(**p) for p in module.peers] + + data["rejected_paths"] = rejected + + for key in data: + setattr(self, key, list(set(data[key]))) + + def __add__(self, query_result): + """Merge the data from both QueryResult objects.""" + return {k: getattr(self, k) + getattr(query_result, k) for k in self.data_keys} + + def __radd__(self, query_result): + """Call self.__add__ since the order of addition does not matter.""" + return self.__add__(query_result) @dataclass @@ -106,6 +149,11 @@ def __post_init__(self, bmo_data): if bmo_data: self.nick = bmo_data.get("nick", "") self.real_name = bmo_data.get("real_name", "") + self.bmo_id = bmo_data.get("id") or self.bmo_id + + def __hash__(self): + """Return a unique identifier for this person.""" + return int(self.bmo_id) class People: diff --git a/tests/conftest.py b/tests/conftest.py index 6e32d68..30bf2ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,9 +57,9 @@ def repo(tmp_path, config): @pytest.fixture def config(): people = [ - {"real_name": "jane", "nick": "jane", "name": "jane", "bmo_id": 0}, - {"real_name": "jill", "nick": "jill", "name": "jill", "bmo_id": 1}, - {"real_name": "otis", "nick": "otis", "name": "otis", "bmo_id": 2}, + {"real_name": "jane", "nick": "jane", "bmo_id": 0}, + {"real_name": "jill", "nick": "jill", "bmo_id": 1}, + {"real_name": "otis", "nick": "otis", "bmo_id": 2}, ] return { "repo": "test_repo", diff --git a/tests/test_directory.py b/tests/test_directory.py index d80ab65..718e8e6 100644 --- a/tests/test_directory.py +++ b/tests/test_directory.py @@ -5,6 +5,7 @@ """Tests for directory module.""" from mots.directory import Directory +from mots.directory import Person from mots.config import FileConfig @@ -89,10 +90,17 @@ def test_directory__Directory__query(repo): "felines/maine_coon", ] - result, rejected = directory.query(*paths_to_check) - assert result == { + result = directory.query(*paths_to_check) + assert result.path_map == { "canines/chihuahuas/apple_head": [directory.modules_by_machine_name["pets"]], "birds/parrot": [directory.modules_by_machine_name["pets"]], "felines/persian": [directory.modules_by_machine_name["pets"]], } - assert rejected == ["felines/maine_coon"] + assert set(result.paths) == { + "canines/chihuahuas/apple_head", + "birds/parrot", + "felines/persian", + } + assert set(result.owners) == {Person(bmo_id=2, real_name='otis', nick='otis')} + assert set(result.peers) == {Person(bmo_id=2, real_name='otis', nick='otis')} + assert result.rejected_paths == ["felines/maine_coon"] From 16ed62e13dd2ae476e3362244bc9523c287c5c1e Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Tue, 30 Nov 2021 15:46:48 -0500 Subject: [PATCH 06/26] Additional improvements: - update QueryResult.__add__ to return a QueryResult object - minor fixes to documentation - add test for QueryResult - update command line usage in docs - temporarily move readme to readthedocs link --- README.md | 52 +----------------- documentation/index.rst | 119 ++++++++++++++++++++++++++++++++++++++-- src/mots/cli.py | 7 ++- src/mots/directory.py | 7 ++- src/mots/module.py | 4 +- src/mots/utils.py | 6 +- tests/test_directory.py | 38 ++++++++++++- 7 files changed, 167 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index d7eb1dd..8427800 100644 --- a/README.md +++ b/README.md @@ -1,51 +1 @@ -Development environment ------------------------ -To set up a local development environment, run the following commands. Replace the python version with the desired version on your local machine. - -.. code-block:: bash - - make dev-env PY=python3.9 - source .mots-env/bin/activate - make dev - -The above commands will set up a local development environment using the provided python version available on your machine, and subsequently install all required packages in that environment. - -Generate coverage report ------------------------- - -To generate a standard coverage report, run: - -.. code-block:: bash - - make cov - -To generate an html coverage report, run: - -.. code-block:: bash - - make cov-html - make serve-cov - -Then navigate to your web browser. - -Other make commands -------------------- -Run `make` to see all available commands. - -.. code-block:: bash - - usage: make - - target is one of: - help show this message and exit - build build the python package and wheels - clean remove temporary files - cov run coverage check - cov-html generate html coverage report - dev setup local dev environment by installing required packages, etc. - dev-env create a python virtual environment in ./mots-env - docs generate documentation - requirements regenerate requirements.txt - serve-cov simple http server for coverage report - serve-docs simple http server for docs - test run the test suite +For full documentation, see https://mots.readthedocs.io/en/develop/. diff --git a/documentation/index.rst b/documentation/index.rst index 23ae75a..d87de4c 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -3,11 +3,6 @@ :caption: Contents: -README -====== - -.. include:: ../README.md - Developer Interface =================== @@ -39,3 +34,117 @@ Utils --------- .. automodule:: mots.utils :members: + +Command Line Usage +================== +You can get this help by typing ``mots`` at the command line. + +usage: mots [-h] [--debug] {init,module,validate,clean,query} ... + +main command line interface for mots + +optional arguments: + -h, --help show this help message and exit + --debug enable debug output + +commands: + {init,module,validate,clean,query} + init initialize mots configuration in repo + module module_operations + validate validate mots config for current repo + clean clean mots config for current repo + query query the module directory + +usage: mots init [-h] [--path PATH] + +optional arguments: + -h, --help show this help message and exit + --path PATH, -p PATH the path of the repo to initialize + +usage: mots module [-h] [--path PATH] {list,add,show} ... + +optional arguments: + -h, --help show this help message and exit + --path PATH, -p PATH the path of the repo config file + +module: + {list,add,show} + list list all modules + add add a new module + show show a module + +usage: mots validate [-h] [--path PATH] [--repo-path REPO_PATH] + +optional arguments: + -h, --help show this help message and exit + --path PATH, -p PATH the path of the repo config file + --repo-path REPO_PATH, -r REPO_PATH + the path of the repo + +usage: mots clean [-h] [--path PATH] [--repo-path REPO_PATH] + +optional arguments: + -h, --help show this help message and exit + --path PATH, -p PATH the path of the repo config file + --repo-path REPO_PATH, -r REPO_PATH + the path of the repo + +usage: mots query [-h] [--path PATH] paths [paths ...] + +positional arguments: + paths a list of paths to query + +optional arguments: + -h, --help show this help message and exit + --path PATH, -p PATH the path of the repo config file + +Development environment +======================= +To set up a local development environment, run the following commands. Replace the python version with the desired version on your local machine. + +.. code-block:: bash + + make dev-env PY=python3.9 + source .mots-env/bin/activate + make dev + +The above commands will set up a local development environment using the provided python version available on your machine, and subsequently install all required packages in that environment. + +Generate coverage report +------------------------ + +To generate a standard coverage report, run: + +.. code-block:: bash + + make cov + +To generate an html coverage report, run: + +.. code-block:: bash + + make cov-html + make serve-cov + +Then navigate to your web browser. + +Other make commands +------------------- +Run `make` to see all available commands. + +.. code-block:: bash + + usage: make + + target is one of: + help show this message and exit + build build the python package and wheels + clean remove temporary files + cov run coverage check + cov-html generate html coverage report + dev setup local dev environment by installing required packages, etc. + dev-env create a python virtual environment in ./mots-env + docs generate documentation + requirements regenerate requirements.txt + serve-cov simple http server for coverage report + serve-docs simple http server for docs diff --git a/src/mots/cli.py b/src/mots/cli.py index 1ea35f7..1ebb4cc 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -109,7 +109,7 @@ def add(args): def main(): """Redirect to appropriate function.""" - parser = create_parser() + parser, subparsers = create_parser() args = parser.parse_args() init_logging(debug=args.debug) @@ -121,6 +121,9 @@ def main(): logger.debug(f"{args.func} took {(et - st).total_seconds()} seconds.") else: parser.print_help() + for name, subparser in subparsers.choices.items(): + print() + subparser.print_help() def create_parser(): @@ -210,4 +213,4 @@ def create_parser(): query_parser.add_argument("paths", nargs="+", help="a list of paths to query") query_parser.set_defaults(func=CLI.query) - return parser + return parser, subparsers diff --git a/src/mots/directory.py b/src/mots/directory.py index 2db904c..58fea21 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -123,12 +123,17 @@ def __init__(self, result, rejected): data["rejected_paths"] = rejected + # Remove duplicate entries in all data attributes. for key in data: setattr(self, key, list(set(data[key]))) def __add__(self, query_result): """Merge the data from both QueryResult objects.""" - return {k: getattr(self, k) + getattr(query_result, k) for k in self.data_keys} + path_map = self.path_map.copy() + path_map.update(query_result.path_map) + rejected_paths = self.rejected_paths.copy() + rejected_paths += query_result.rejected_paths + return QueryResult(path_map, rejected_paths) def __radd__(self, query_result): """Call self.__add__ since the order of addition does not matter.""" diff --git a/src/mots/module.py b/src/mots/module.py index 39d6411..5b15c9e 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -226,7 +226,7 @@ def clean(file_config: FileConfig, write: bool = True): there is no valid `machine_name`, generate one. Write changes to disk. :param file_config: an instance of :class:`FileConfig` - :param bool: if set to `True`, writes changes to disk. + :param write: if set to `True`, writes changes to disk. """ from mots.directory import Directory @@ -336,7 +336,7 @@ def add( :param module: a dictionary containing module parameters :param file_config: an instance of :class:`FileConfig` :param parent: the machine name of the parent module if applicable - :param bool: if set to `True`, writes changes to disk + :param write: if set to `True`, writes changes to disk """ file_config.load() modules = file_config.config["modules"] diff --git a/src/mots/utils.py b/src/mots/utils.py index 5ee6495..d563635 100644 --- a/src/mots/utils.py +++ b/src/mots/utils.py @@ -34,9 +34,9 @@ def parse_user_string(string): """Return user data based on provided string. Example: - >>> test = parse_user_string("charlie jones (cj) ") - >>> print(test) - >>> {"meta": "cj", "email": "cjones@example.org", "name": "charlie jones"} + >>> test = parse_user_string("charlie jones (cj) ") + >>> print(test) + >>> {"meta": "cj", "email": "cjones@example.org", "name": "charlie jones"} """ logger.debug(f"Parsing provided string {string}...") pattern = re.compile( diff --git a/tests/test_directory.py b/tests/test_directory.py index 718e8e6..d35f5a9 100644 --- a/tests/test_directory.py +++ b/tests/test_directory.py @@ -101,6 +101,40 @@ def test_directory__Directory__query(repo): "birds/parrot", "felines/persian", } - assert set(result.owners) == {Person(bmo_id=2, real_name='otis', nick='otis')} - assert set(result.peers) == {Person(bmo_id=2, real_name='otis', nick='otis')} + assert set(result.owners) == {Person(bmo_id=2, real_name="otis", nick="otis")} + assert set(result.peers) == {Person(bmo_id=2, real_name="otis", nick="otis")} + assert result.rejected_paths == ["felines/maine_coon"] + + +def test_directory__Directory__query_merging(repo): + file_config = FileConfig(repo / "mots.yml") + directory = Directory(file_config) + directory.load(query_bmo=False) + + paths_to_check_1 = [ + "canines/chihuahuas/apple_head", + "birds/parrot", + ] + paths_to_check_2 = [ + "felines/persian", + "felines/maine_coon", + ] + + result_1 = directory.query(*paths_to_check_1) + result_2 = directory.query(*paths_to_check_2) + + result = result_1 + result_2 + + assert result.path_map == { + "canines/chihuahuas/apple_head": [directory.modules_by_machine_name["pets"]], + "birds/parrot": [directory.modules_by_machine_name["pets"]], + "felines/persian": [directory.modules_by_machine_name["pets"]], + } + assert set(result.paths) == { + "canines/chihuahuas/apple_head", + "birds/parrot", + "felines/persian", + } + assert set(result.owners) == {Person(bmo_id=2, real_name="otis", nick="otis")} + assert set(result.peers) == {Person(bmo_id=2, real_name="otis", nick="otis")} assert result.rejected_paths == ["felines/maine_coon"] From e5c31c6ba28abfe5fe8ef1aaae85cd2dc1cda714 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Fri, 3 Dec 2021 10:20:01 -0500 Subject: [PATCH 07/26] Additional improvements: - fix QueryResult.rejected_paths sum to find intersection only - add wiki export functionality (bug 1743476) --- src/mots/cli.py | 24 +++++++++++++++++ src/mots/directory.py | 60 +++++++++++++++++++++++++++++++++++++++-- src/mots/utils.py | 3 ++- tests/test_directory.py | 1 + 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/mots/cli.py b/src/mots/cli.py index 1ebb4cc..3ff557e 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -106,6 +106,16 @@ def add(args): file_config = FileConfig(Path(args.path)) module.add(params, file_config, parent=parent, write=True) + @staticmethod + def export(args): + """Call `directory.export` with relevant parameters.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + directory = Directory(file_config) + directory.load() + output = directory.export(args.format) + print(output) + def main(): """Redirect to appropriate function.""" @@ -115,6 +125,7 @@ def main(): init_logging(debug=args.debug) if hasattr(args, "func"): + logger.debug(f"Calling {args.func} with {args}...") st = datetime.now() args.func(args) et = datetime.now() @@ -213,4 +224,17 @@ def create_parser(): query_parser.add_argument("paths", nargs="+", help="a list of paths to query") query_parser.set_defaults(func=CLI.query) + export_parser = subparsers.add_parser("export", help="export the module directory") + export_parser.add_argument( + "--path", + "-p", + type=str, + help="the path of the repo config file", + default=DEFAULT_CONFIG_FILEPATH, + ) + export_parser.add_argument( + "--format", "-f", type=str, default="wiki", help="a list of paths to export" + ) + export_parser.set_defaults(func=CLI.export) + return parser, subparsers diff --git a/src/mots/directory.py b/src/mots/directory.py index 58fea21..7f94687 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -16,6 +16,45 @@ logger = logging.getLogger(__name__) +WIKI_TMPL = """ +{% macro people_entry(people) %} +{%- for p in people %}{{ p.nick }}{% if not loop.last %}, {% endif %}{% endfor %} +{% endmacro %} + +{%- macro module_entry(module) -%} +{% raw %}{{Module{% endraw %} +|name={{ module.name }} +|description={{ module.description }} +|owner(s)={{ people_entry(module.owners) -}} +|peer(s)={{ people_entry(module.peers) -}} +|includes={{ module.includes|join(", ") }} +|excludes={{ module.excludes|join(", ") }} +{%- if module.meta.group -%} +|group={{ module.meta.group }} +{% endif %} +{%- if module.meta.url -%} +|url={{ module.meta.group }} +{% endif %} +{%- if module.meta.components -%} +|url={{ module.meta.components }} +{% endif %} +{% raw %}}}{% endraw %} +{% endmacro %} + + +{{ directory.description or "No directory description provided." }} + +{%- for module in directory.modules -%} +{{ module_entry(module) }} +{% if module.submodules %} +== Submodules == +{% for submodule in module.submodules %} +{{ module_entry(submodule) }} +{% endfor %} +{% endif %} +{% endfor %} +""" + class Directory: """Mots directory and path index.""" @@ -89,6 +128,21 @@ def query(self, *paths: str) -> QueryResult: return QueryResult(result, rejected) + def _export_wiki(self): + from jinja2 import Template + + template = Template(WIKI_TMPL) + out = template.render(directory=self) + return out + + def export(self, frmt="wiki"): + """Export directory in a specified format.""" + supported_formats = ["wiki"] + if frmt not in supported_formats: + raise ValueError(f"{frmt} not one of {supported_formats}.") + + return getattr(self, f"_export_{frmt}")() + class QueryResult: """Helper class to simplify query result interpretation.""" @@ -131,8 +185,10 @@ def __add__(self, query_result): """Merge the data from both QueryResult objects.""" path_map = self.path_map.copy() path_map.update(query_result.path_map) - rejected_paths = self.rejected_paths.copy() - rejected_paths += query_result.rejected_paths + rejected_paths = set.intersection( + set(self.rejected_paths), + set(query_result.rejected_paths), + ) return QueryResult(path_map, rejected_paths) def __radd__(self, query_result): diff --git a/src/mots/utils.py b/src/mots/utils.py index d563635..83a2e01 100644 --- a/src/mots/utils.py +++ b/src/mots/utils.py @@ -27,7 +27,8 @@ def get_list_input(text: str): :param test: the text to prompt the user with """ - return [o.strip() for o in input(f"{text}: ").split(",") if o] + user_input = input(f"{text}: ").split(",") + return [e.strip() for e in user_input if e] def parse_user_string(string): diff --git a/tests/test_directory.py b/tests/test_directory.py index d35f5a9..8d1616a 100644 --- a/tests/test_directory.py +++ b/tests/test_directory.py @@ -114,6 +114,7 @@ def test_directory__Directory__query_merging(repo): paths_to_check_1 = [ "canines/chihuahuas/apple_head", "birds/parrot", + "felines/maine_coon", ] paths_to_check_2 = [ "felines/persian", From 9dfac7e2be4ef19f2fe12db951da71e011273554 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Wed, 8 Dec 2021 13:33:50 -0500 Subject: [PATCH 08/26] Additional improvements: - add hashes to requirements file + make command --- Makefile | 2 +- requirements.txt | 574 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 505 insertions(+), 71 deletions(-) diff --git a/Makefile b/Makefile index 345b8bb..686a2c7 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ format: .PHONY: requirements requirements: $ rm requirements.txt - $(PYTHON) -m piptools compile + $(PYTHON) -m piptools compile --generate-hashes .ONESHELL: .PHONY: dev-env diff --git a/requirements.txt b/requirements.txt index acda099..f2132d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,173 +2,607 @@ # This file is autogenerated by pip-compile with python 3.8 # To update, run: # -# pip-compile +# pip-compile --generate-hashes # -alabaster==0.7.12 +alabaster==0.7.12 \ + --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ + --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 # via sphinx -appdirs==1.4.4 +appdirs==1.4.4 \ + --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ + --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 # via black -attrs==21.2.0 +attrs==21.2.0 \ + --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ + --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb # via pytest -babel==2.9.1 +babel==2.9.1 \ + --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ + --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 # via sphinx -black==21.7b0 +black==21.7b0 \ + --hash=sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116 \ + --hash=sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219 # via -r requirements.in -bleach==4.1.0 +bleach==4.1.0 \ + --hash=sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da \ + --hash=sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994 # via readme-renderer -build==0.6.0.post1 +build==0.6.0.post1 \ + --hash=sha256:32290592c8ccf70ce84107962f6129407abf52cedaa752af28c0c95d99dfa2e7 \ + --hash=sha256:d8d8417caff47888274d677f984de509554637dd1ea952d467b027849b06d83b # via -r requirements.in -certifi==2021.5.30 +certifi==2021.10.8 \ + --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ + --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 # via requests -cffi==1.14.6 +cffi==1.15.0 \ + --hash=sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3 \ + --hash=sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2 \ + --hash=sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636 \ + --hash=sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20 \ + --hash=sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728 \ + --hash=sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27 \ + --hash=sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66 \ + --hash=sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443 \ + --hash=sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0 \ + --hash=sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7 \ + --hash=sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39 \ + --hash=sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605 \ + --hash=sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a \ + --hash=sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37 \ + --hash=sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029 \ + --hash=sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139 \ + --hash=sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc \ + --hash=sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df \ + --hash=sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14 \ + --hash=sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880 \ + --hash=sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2 \ + --hash=sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a \ + --hash=sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e \ + --hash=sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474 \ + --hash=sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024 \ + --hash=sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8 \ + --hash=sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0 \ + --hash=sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e \ + --hash=sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a \ + --hash=sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e \ + --hash=sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032 \ + --hash=sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6 \ + --hash=sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e \ + --hash=sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b \ + --hash=sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e \ + --hash=sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954 \ + --hash=sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962 \ + --hash=sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c \ + --hash=sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4 \ + --hash=sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55 \ + --hash=sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962 \ + --hash=sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023 \ + --hash=sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c \ + --hash=sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6 \ + --hash=sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8 \ + --hash=sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382 \ + --hash=sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7 \ + --hash=sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc \ + --hash=sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997 \ + --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796 # via cryptography -charset-normalizer==2.0.6 +charset-normalizer==2.0.9 \ + --hash=sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721 \ + --hash=sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c # via requests -click==8.0.1 +click==8.0.3 \ + --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ + --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b # via black -colorama==0.4.4 +colorama==0.4.4 \ + --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ + --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via twine -coverage==6.0 +coverage==6.2 \ + --hash=sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0 \ + --hash=sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd \ + --hash=sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884 \ + --hash=sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48 \ + --hash=sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76 \ + --hash=sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0 \ + --hash=sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64 \ + --hash=sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685 \ + --hash=sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47 \ + --hash=sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d \ + --hash=sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840 \ + --hash=sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f \ + --hash=sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971 \ + --hash=sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c \ + --hash=sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a \ + --hash=sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de \ + --hash=sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17 \ + --hash=sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4 \ + --hash=sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521 \ + --hash=sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57 \ + --hash=sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b \ + --hash=sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282 \ + --hash=sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644 \ + --hash=sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475 \ + --hash=sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d \ + --hash=sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da \ + --hash=sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953 \ + --hash=sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2 \ + --hash=sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e \ + --hash=sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c \ + --hash=sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc \ + --hash=sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64 \ + --hash=sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74 \ + --hash=sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617 \ + --hash=sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3 \ + --hash=sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d \ + --hash=sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa \ + --hash=sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739 \ + --hash=sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8 \ + --hash=sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8 \ + --hash=sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781 \ + --hash=sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58 \ + --hash=sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9 \ + --hash=sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c \ + --hash=sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd \ + --hash=sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e \ + --hash=sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49 # via pytest-cov -cryptography==35.0.0 +cryptography==36.0.0 \ + --hash=sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681 \ + --hash=sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed \ + --hash=sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4 \ + --hash=sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568 \ + --hash=sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e \ + --hash=sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f \ + --hash=sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f \ + --hash=sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712 \ + --hash=sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e \ + --hash=sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58 \ + --hash=sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44 \ + --hash=sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6 \ + --hash=sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d \ + --hash=sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636 \ + --hash=sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba \ + --hash=sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120 \ + --hash=sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3 \ + --hash=sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d \ + --hash=sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b \ + --hash=sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81 \ + --hash=sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8 # via secretstorage -docutils==0.17.1 +docutils==0.17.1 \ + --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ + --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 # via # readme-renderer # sphinx # sphinx-rtd-theme -flake8==3.9.2 +flake8==3.9.2 \ + --hash=sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b \ + --hash=sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907 # via # -r requirements.in # flake8-docstrings -flake8-docstrings==1.6.0 +flake8-docstrings==1.6.0 \ + --hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \ + --hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b # via -r requirements.in -idna==3.2 +idna==3.3 \ + --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ + --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d # via requests -imagesize==1.2.0 +imagesize==1.3.0 \ + --hash=sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c \ + --hash=sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d # via sphinx -importlib-metadata==4.8.1 +importlib-metadata==4.8.2 \ + --hash=sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100 \ + --hash=sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb # via # keyring # twine -iniconfig==1.1.1 +iniconfig==1.1.1 \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 # via pytest -jeepney==0.7.1 +jeepney==0.7.1 \ + --hash=sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac \ + --hash=sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f # via # keyring # secretstorage -jinja2==3.0.1 +jinja2==3.0.3 \ + --hash=sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 \ + --hash=sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7 # via sphinx -keyring==23.2.1 +keyring==23.4.0 \ + --hash=sha256:3dc0f66062a4f8f6f2ce30d6a516e6e623e6c3c2e76864204ceaf64695408f07 \ + --hash=sha256:88f206024295e3c6fb16bb0a60fb4bb7ec1185629dc5a729f12aa7c236d01387 # via twine -markupsafe==2.0.1 +markupsafe==2.0.1 \ + --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ + --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ + --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ + --hash=sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194 \ + --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ + --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ + --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ + --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ + --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ + --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ + --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ + --hash=sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a \ + --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ + --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ + --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ + --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ + --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ + --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ + --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ + --hash=sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047 \ + --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ + --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ + --hash=sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b \ + --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ + --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ + --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ + --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a \ + --hash=sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1 \ + --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ + --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ + --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ + --hash=sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee \ + --hash=sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f \ + --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ + --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ + --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ + --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ + --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ + --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ + --hash=sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86 \ + --hash=sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6 \ + --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ + --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ + --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ + --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ + --hash=sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e \ + --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ + --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ + --hash=sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f \ + --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ + --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ + --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ + --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ + --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ + --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ + --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ + --hash=sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a \ + --hash=sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207 \ + --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ + --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ + --hash=sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd \ + --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ + --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ + --hash=sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9 \ + --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ + --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ + --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ + --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ + --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 # via jinja2 -mccabe==0.6.1 +mccabe==0.6.1 \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f # via flake8 -mypy-extensions==0.4.3 +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 # via black -packaging==21.0 +packaging==21.3 \ + --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ + --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 # via # bleach # build # pytest # sphinx -pathspec==0.9.0 +pathspec==0.9.0 \ + --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ + --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 # via black -pep517==0.11.0 +pep517==0.12.0 \ + --hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \ + --hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161 # via build -pkginfo==1.7.1 +pkginfo==1.8.2 \ + --hash=sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff \ + --hash=sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc # via twine -pluggy==1.0.0 +pluggy==1.0.0 \ + --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ + --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 # via pytest -py==1.10.0 +py==1.11.0 \ + --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ + --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 # via pytest -pycodestyle==2.7.0 +pycodestyle==2.7.0 \ + --hash=sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068 \ + --hash=sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef # via flake8 -pycparser==2.20 +pycparser==2.21 \ + --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ + --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 # via cffi -pydocstyle==6.1.1 +pydocstyle==6.1.1 \ + --hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \ + --hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4 # via flake8-docstrings -pyflakes==2.3.1 +pyflakes==2.3.1 \ + --hash=sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3 \ + --hash=sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db # via flake8 -pygments==2.10.0 +pygments==2.10.0 \ + --hash=sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380 \ + --hash=sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6 # via # readme-renderer # sphinx -pyparsing==2.4.7 +pyparsing==3.0.6 \ + --hash=sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4 \ + --hash=sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81 # via packaging -pytest==6.2.5 +pytest==6.2.5 \ + --hash=sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89 \ + --hash=sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134 # via # -r requirements.in # pytest-cov -pytest-cov==2.12.1 +pytest-cov==2.12.1 \ + --hash=sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a \ + --hash=sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7 # via -r requirements.in -pytz==2021.3 +pytz==2021.3 \ + --hash=sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c \ + --hash=sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326 # via babel -pyyaml==5.4.1 +pyyaml==5.4.1 \ + --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ + --hash=sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696 \ + --hash=sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393 \ + --hash=sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77 \ + --hash=sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922 \ + --hash=sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5 \ + --hash=sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8 \ + --hash=sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10 \ + --hash=sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc \ + --hash=sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018 \ + --hash=sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e \ + --hash=sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253 \ + --hash=sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347 \ + --hash=sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183 \ + --hash=sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541 \ + --hash=sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb \ + --hash=sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185 \ + --hash=sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc \ + --hash=sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db \ + --hash=sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa \ + --hash=sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46 \ + --hash=sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122 \ + --hash=sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b \ + --hash=sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63 \ + --hash=sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df \ + --hash=sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc \ + --hash=sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247 \ + --hash=sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6 \ + --hash=sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0 # via -r requirements.in -readme-renderer==30.0 +readme-renderer==30.0 \ + --hash=sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc \ + --hash=sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8 # via twine -regex==2021.9.30 +regex==2021.11.10 \ + --hash=sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05 \ + --hash=sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f \ + --hash=sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc \ + --hash=sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4 \ + --hash=sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737 \ + --hash=sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a \ + --hash=sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4 \ + --hash=sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8 \ + --hash=sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d \ + --hash=sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03 \ + --hash=sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f \ + --hash=sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264 \ + --hash=sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a \ + --hash=sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef \ + --hash=sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f \ + --hash=sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da \ + --hash=sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc \ + --hash=sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063 \ + --hash=sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50 \ + --hash=sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a \ + --hash=sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49 \ + --hash=sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d \ + --hash=sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d \ + --hash=sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733 \ + --hash=sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00 \ + --hash=sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b \ + --hash=sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a \ + --hash=sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36 \ + --hash=sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345 \ + --hash=sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0 \ + --hash=sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732 \ + --hash=sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286 \ + --hash=sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12 \ + --hash=sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646 \ + --hash=sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667 \ + --hash=sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244 \ + --hash=sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29 \ + --hash=sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec \ + --hash=sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf \ + --hash=sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4 \ + --hash=sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449 \ + --hash=sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0 \ + --hash=sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a \ + --hash=sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d \ + --hash=sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129 \ + --hash=sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb \ + --hash=sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e \ + --hash=sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b \ + --hash=sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83 \ + --hash=sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf \ + --hash=sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e \ + --hash=sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b \ + --hash=sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942 \ + --hash=sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a \ + --hash=sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e \ + --hash=sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94 \ + --hash=sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc \ + --hash=sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a \ + --hash=sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e \ + --hash=sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965 \ + --hash=sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0 \ + --hash=sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36 \ + --hash=sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296 \ + --hash=sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec \ + --hash=sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23 \ + --hash=sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7 \ + --hash=sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe \ + --hash=sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6 \ + --hash=sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8 \ + --hash=sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b \ + --hash=sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb \ + --hash=sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b \ + --hash=sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30 \ + --hash=sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e # via black -requests==2.26.0 +requests==2.26.0 \ + --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \ + --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 # via # requests-toolbelt # sphinx # twine -requests-toolbelt==0.9.1 +requests-toolbelt==0.9.1 \ + --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \ + --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 # via twine -rfc3986==1.5.0 +rfc3986==1.5.0 \ + --hash=sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835 \ + --hash=sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97 # via twine -ruamel.yaml==0.17.16 +ruamel.yaml==0.17.16 \ + --hash=sha256:1a771fc92d3823682b7f0893ad56cb5a5c87c48e62b5399d6f42c8759a583b33 \ + --hash=sha256:ea21da1198c4b41b8e7a259301cc9710d3b972bf8ba52f06218478e6802dd1f1 # via -r requirements.in -ruamel.yaml.clib==0.2.6 +ruamel.yaml.clib==0.2.6 \ + --hash=sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd \ + --hash=sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0 \ + --hash=sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277 \ + --hash=sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104 \ + --hash=sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd \ + --hash=sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78 \ + --hash=sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99 \ + --hash=sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527 \ + --hash=sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84 \ + --hash=sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7 \ + --hash=sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468 \ + --hash=sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b \ + --hash=sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94 \ + --hash=sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233 \ + --hash=sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb \ + --hash=sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5 \ + --hash=sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe \ + --hash=sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751 \ + --hash=sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502 \ + --hash=sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed \ + --hash=sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c # via ruamel.yaml -secretstorage==3.3.1 +secretstorage==3.3.1 \ + --hash=sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f \ + --hash=sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195 # via keyring -six==1.16.0 +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # via bleach -snowballstemmer==2.1.0 +snowballstemmer==2.2.0 \ + --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ + --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a # via # pydocstyle # sphinx -sphinx==4.2.0 +sphinx==4.2.0 \ + --hash=sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6 \ + --hash=sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0 # via # -r requirements.in # sphinx-rtd-theme -sphinx-rtd-theme==1.0.0 +sphinx-rtd-theme==1.0.0 \ + --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \ + --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c # via -r requirements.in -sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-applehelp==1.0.2 \ + --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ + --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 \ + --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ + --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 # via sphinx -sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-htmlhelp==2.0.0 \ + --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \ + --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 # via sphinx -sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.3 \ + --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ + --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.5 \ + --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ + --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 # via sphinx -toml==0.10.2 +toml==0.10.2 \ + --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ + --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f # via # pytest # pytest-cov -tomli==1.2.1 +tomli==1.2.2 \ + --hash=sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee \ + --hash=sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade # via # black # build # pep517 -tqdm==4.62.3 +tqdm==4.62.3 \ + --hash=sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c \ + --hash=sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d # via twine -twine==3.4.2 +twine==3.4.2 \ + --hash=sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218 \ + --hash=sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936 # via -r requirements.in -urllib3==1.26.7 +urllib3==1.26.7 \ + --hash=sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece \ + --hash=sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844 # via requests -webencodings==0.5.1 +webencodings==0.5.1 \ + --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ + --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 # via bleach -zipp==3.6.0 +zipp==3.6.0 \ + --hash=sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832 \ + --hash=sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc # via importlib-metadata -# The following packages are considered to be unsafe in a requirements file: +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. # setuptools From 15373dffe8dc902b5efa37d50cf166497c44d2c0 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Fri, 7 Jan 2022 15:13:40 -0500 Subject: [PATCH 09/26] Additional improvements: - use isoformat for timestamps - add parse_real_name method to parse bmo data into name and info --- src/mots/config.py | 5 ++- src/mots/directory.py | 11 +++++-- src/mots/utils.py | 10 ++++++ tests/conftest.py | 6 ++-- tests/test_directory.py | 16 +++++++--- tests/test_utils.py | 68 ++++++++++++++++++++++++++++++++++++++++- 6 files changed, 102 insertions(+), 14 deletions(-) diff --git a/src/mots/config.py b/src/mots/config.py index c6c6c8e..598bf25 100644 --- a/src/mots/config.py +++ b/src/mots/config.py @@ -13,7 +13,6 @@ yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) -yaml.default_style = '""' DEFAULT_CONFIG_FILEPATH = "./mots.yaml" @@ -33,7 +32,7 @@ def init(self): """Initialize a repo with a config file, if it does not contain it.""" if not self.path.is_file(): # File does not exist, create it. - now = datetime.now() + now = datetime.now().isoformat() self.config = { "repo": str(Path(self.path).resolve().parts[-2]), "created_at": now, @@ -52,6 +51,6 @@ def load(self): def write(self): """Write configuration to file, and update the timestamp.""" - self.config["updated_at"] = datetime.now() + self.config["updated_at"] = datetime.now().isoformat() with open(self.path, "w") as f: yaml.dump(self.config, f) diff --git a/src/mots/directory.py b/src/mots/directory.py index 7f94687..0f64324 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -13,6 +13,7 @@ from mots.bmo import BMOClient from mots.module import Module from mots.config import FileConfig +from mots.utils import parse_real_name logger = logging.getLogger(__name__) @@ -201,7 +202,8 @@ class Person: """A class representing a person.""" bmo_id: int = None - real_name: str = "" + name: str = "" + info: str = "" nick: str = "" bmo_data: InitVar[dict] = None @@ -209,8 +211,12 @@ def __post_init__(self, bmo_data): """Refresh BMO data from BMO API.""" if bmo_data: self.nick = bmo_data.get("nick", "") - self.real_name = bmo_data.get("real_name", "") self.bmo_id = bmo_data.get("id") or self.bmo_id + real_name = bmo_data.get("real_name", "") + + parsed_real_name = parse_real_name(real_name) + self.name = parsed_real_name["name"] + self.info = parsed_real_name["info"] def __hash__(self): """Return a unique identifier for this person.""" @@ -239,7 +245,6 @@ def __init__(self, people, query_bmo: bool = True): logger.debug(f"Adding person {p} to roster...") self.people.append( Person( - real_name=p["real_name"], bmo_id=p["bmo_id"], bmo_data=bmo_data.get(p["bmo_id"]), ) diff --git a/src/mots/utils.py b/src/mots/utils.py index 83a2e01..8211350 100644 --- a/src/mots/utils.py +++ b/src/mots/utils.py @@ -47,3 +47,13 @@ def parse_user_string(string): match = pattern.match(string) if match: return match.groupdict() + + +def parse_real_name(real_name): + """Parse real_name into name and info.""" + pattern = re.compile(r"^(?P[\w\ ]+?)?\ ?(?P[\(\[\|\:].*)?$") + match = pattern.match(real_name) + if match: + return match.groupdict() + else: + return {"name": None, "info": None} diff --git a/tests/conftest.py b/tests/conftest.py index 30bf2ad..271759c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,9 +57,9 @@ def repo(tmp_path, config): @pytest.fixture def config(): people = [ - {"real_name": "jane", "nick": "jane", "bmo_id": 0}, - {"real_name": "jill", "nick": "jill", "bmo_id": 1}, - {"real_name": "otis", "nick": "otis", "bmo_id": 2}, + {"info": "testing", "name": "jane", "nick": "jane", "bmo_id": 0}, + {"info": "testing", "name": "jill", "nick": "jill", "bmo_id": 1}, + {"info": "testing", "name": "otis", "nick": "otis", "bmo_id": 2}, ] return { "repo": "test_repo", diff --git a/tests/test_directory.py b/tests/test_directory.py index 8d1616a..81d617a 100644 --- a/tests/test_directory.py +++ b/tests/test_directory.py @@ -101,8 +101,12 @@ def test_directory__Directory__query(repo): "birds/parrot", "felines/persian", } - assert set(result.owners) == {Person(bmo_id=2, real_name="otis", nick="otis")} - assert set(result.peers) == {Person(bmo_id=2, real_name="otis", nick="otis")} + assert set(result.owners) == { + Person(bmo_id=2, name="otis", info="testing", nick="otis") + } + assert set(result.peers) == { + Person(bmo_id=2, name="otis", info="testing", nick="otis") + } assert result.rejected_paths == ["felines/maine_coon"] @@ -136,6 +140,10 @@ def test_directory__Directory__query_merging(repo): "birds/parrot", "felines/persian", } - assert set(result.owners) == {Person(bmo_id=2, real_name="otis", nick="otis")} - assert set(result.peers) == {Person(bmo_id=2, real_name="otis", nick="otis")} + assert set(result.owners) == { + Person(bmo_id=2, name="otis", info="testing", nick="otis") + } + assert set(result.peers) == { + Person(bmo_id=2, name="otis", info="testing", nick="otis") + } assert result.rejected_paths == ["felines/maine_coon"] diff --git a/tests/test_utils.py b/tests/test_utils.py index c457cca..07fb345 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,7 +4,11 @@ """Tests for utils module.""" -from mots.utils import generate_machine_readable_name, parse_user_string +from mots.utils import ( + generate_machine_readable_name, + parse_user_string, + parse_real_name, +) def test_generate_machine_readable_name(): @@ -48,3 +52,65 @@ def test_parse_user_string(): test = parse_user_string("") assert test is None + + +def test_parse_real_name(): + assert parse_real_name("tëstér testerson (:test) [te/st]") == { + "name": "tëstér testerson", + "info": "(:test) [te/st]", + } + + assert parse_real_name("tester testerson [:test] (te/st)") == { + "name": "tester testerson", + "info": "[:test] (te/st)", + } + + assert parse_real_name("tester testerson (te/st)") == { + "name": "tester testerson", + "info": "(te/st)", + } + + assert parse_real_name("tester testerson :test") == { + "name": "tester testerson", + "info": ":test", + } + + assert parse_real_name("tester testerson | TEST | te/st") == { + "name": "tester testerson", + "info": "| TEST | te/st", + } + + assert parse_real_name("tester testerson") == { + "name": "tester testerson", + "info": None, + } + + assert parse_real_name("tester") == { + "name": "tester", + "info": None, + } + + assert parse_real_name("(:bees)") == { + "name": None, + "info": "(:bees)", + } + + assert parse_real_name("[:bees]") == { + "name": None, + "info": "[:bees]", + } + + assert parse_real_name("[bees]") == { + "name": None, + "info": "[bees]", + } + + assert parse_real_name("(bees)") == { + "name": None, + "info": "(bees)", + } + + assert parse_real_name("") == { + "name": None, + "info": None, + } From ec11d39db9dba9ff7f5ec03c905689c953e3a9cc Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Tue, 11 Jan 2022 09:15:52 -0500 Subject: [PATCH 10/26] Small fixes: - fix typo in command help - fix query command output - fix FileConfig default argument when loading config --- setup.cfg | 2 +- src/mots/cli.py | 11 +++++------ src/mots/config.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/setup.cfg b/setup.cfg index d21da6c..3d2d162 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ long_description_content_type = text/markdown url = https://github.com/mozilla-conduit/mots classifiers = License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) - Development Status :: 1 - Planning + Development Status :: 2 - Pre-Alpha Intended Audience :: Developers Operating System :: OS Independent Programming Language :: Python :: 3 diff --git a/src/mots/cli.py b/src/mots/cli.py index 3ff557e..2daa395 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -80,12 +80,11 @@ def query(args): file_config.load() directory = Directory(file_config) directory.load() - result, rejected = directory.query(*args.paths) - for path in result: - modules = result[path] + result = directory.query(*args.paths) + for path in result.path_map: + modules = result.path_map[path] module_names = ",".join([m.machine_name for m in modules]) - owners = ",".join([",".join(m.owners) for m in modules]) - sys.stdout.write(f"{path}:{module_names}:{owners}\n") + sys.stdout.write(f"{path}:{module_names}\n") @staticmethod def add(args): @@ -233,7 +232,7 @@ def create_parser(): default=DEFAULT_CONFIG_FILEPATH, ) export_parser.add_argument( - "--format", "-f", type=str, default="wiki", help="a list of paths to export" + "--format", "-f", type=str, default="wiki", help="the format of exported data" ) export_parser.set_defaults(func=CLI.export) diff --git a/src/mots/config.py b/src/mots/config.py index 598bf25..ab8b74e 100644 --- a/src/mots/config.py +++ b/src/mots/config.py @@ -14,7 +14,7 @@ yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) -DEFAULT_CONFIG_FILEPATH = "./mots.yaml" +DEFAULT_CONFIG_FILEPATH = Path("./mots.yaml") class FileConfig: From 2c7b567b364b69aa3f5a3566f477ddd2875e0a92 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Fri, 28 Jan 2022 14:11:26 -0500 Subject: [PATCH 11/26] Small fixes: - fix peers attribute on module - fix description attribute on module - return submodules and parent when serializing --- setup.cfg | 2 +- src/mots/module.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3d2d162..7fc8e34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ [metadata] name = mots -version = 0.0.1a0 +version = 0.0.1.a0 author = Zeid Zabaneh author_email = zeid@mozilla.com description = Module Ownership in Tree System diff --git a/src/mots/module.py b/src/mots/module.py index 5b15c9e..481eb28 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -61,13 +61,14 @@ def __init__( ): self.name = name self.machine_name = machine_name + self.description = description self.repo_path = Path(repo_path) self.parent = parent self.excludes = excludes or [] self.includes = includes or [] self.owners = owners or [] - self.peers = owners or [] + self.peers = peers or [] self.submodules = [] self.exclude_submodule_paths = exclude_submodule_paths self.exclude_module_paths = exclude_module_paths @@ -139,6 +140,11 @@ def serialize(self): "meta": self.meta, } + if self.submodules: + serialized["submodules"] = [sm.serialize() for sm in self.submodules] + if self.parent: + serialized["parent"] = self.parent.machine_name + return serialized def validate(self, errors=None): From fa5d48f5025b96aaafacb5b5325087bc234c2d24 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Fri, 28 Jan 2022 14:59:38 -0500 Subject: [PATCH 12/26] Code review feedback --- Makefile | 6 +- documentation/conf.py | 19 ++--- src/mots/bmo.py | 62 +++------------ src/mots/cli.py | 168 +++++++++++++++++++--------------------- src/mots/directory.py | 3 +- src/mots/module.py | 43 +++------- src/mots/pmo.py | 4 + tests/conftest.py | 2 + tests/test_directory.py | 4 +- tests/test_module.py | 12 ++- 10 files changed, 135 insertions(+), 188 deletions(-) diff --git a/Makefile b/Makefile index 686a2c7..d475b33 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,7 @@ serve-cov: format: $(PYTHON) -m black src/mots $(PYTHON) -m black tests + $(PYTHON) -m black documentation .PHONY: requirements requirements: @@ -66,13 +67,14 @@ requirements: .ONESHELL: .PHONY: dev-env dev-env: -# TODO Make this better. ifdef PY @echo "Creating virtual env in .mots-env using provided Python ($(PY))..." -@(/usr/bin/$(PY) -m venv .mots-env && ([ $$? -eq 0 ] && echo "$(PY) found.")) 2>/dev/null|| -@(/usr/local/bin/$(PY) -m venv .mots-env && ([ $$? -eq 0 ] && echo "$(PY) found.")) 2>/dev/null else - @echo "PY variable not defined, please pass a python version (e.g. PY=python3.8)" + @echo "PY variable not defined, using python3 if available..." + @echo "Creating virtual env in .mots-env using python3..." + -@(python3 -m venv .mots-env && ([ $$? -eq 0 ] && echo "Found $(shell python3 --version)")) 2>/dev/null endif .PHONY: dev diff --git a/documentation/conf.py b/documentation/conf.py index 94b8499..65b4d62 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -16,14 +16,15 @@ # import os import sys -sys.path.insert(0, os.path.abspath('../src')) + +sys.path.insert(0, os.path.abspath("../src")) # -- Project information ----------------------------------------------------- -project = 'mots' -copyright = '2021 Mozilla Inc.' -author = 'Zeid Zabaneh ' +project = "mots" +copyright = "2021 Mozilla Inc." +author = "Zeid Zabaneh " # -- General configuration --------------------------------------------------- @@ -32,16 +33,16 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', + "sphinx.ext.autodoc", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- @@ -49,9 +50,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] diff --git a/src/mots/bmo.py b/src/mots/bmo.py index 18c96ca..000f9d8 100644 --- a/src/mots/bmo.py +++ b/src/mots/bmo.py @@ -1,7 +1,8 @@ -"""Module that provides helpers to interact with the Bugzilla API.""" +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# TODO: this should be abstracted so that it can be hot-swappable with any API that can -# be searched. +"""Module that provides helpers to interact with the Bugzilla API.""" from __future__ import annotations import requests @@ -14,8 +15,7 @@ DEFAULT_BASE_URL = "https://bugzilla.mozilla.org/rest" USER_AGENT = "mots" # TODO: improve this and include version. - -# TODO set NO_INPUT var to prevent prompt. +BUGZILLA_API_KEY_ENV_VAR = "BUGZILLA_API_KEY" class BMOClient: @@ -23,11 +23,12 @@ class BMOClient: def __init__(self, token: str = None, base_url: str = DEFAULT_BASE_URL): if not token: - token = os.getenv("BUGZILLA_API_KEY", "") + token = os.getenv(BUGZILLA_API_KEY_ENV_VAR, "") if not token: - token = input("Enter BMO Token: ") - if not token: - raise Exception() + raise ValueError( + f"{BUGZILLA_API_KEY_ENV_VAR} environment variable missing," + " and no other token was explicitly provided" + ) self.headers = {"X-BUGZILLA-API-KEY": token, "User-Agent": USER_AGENT} self.base_url = base_url @@ -39,40 +40,8 @@ def _get(self, path: str, params=None): ) return response - def get_user(self, email: str, rate_limit_delay: float = 0.3): - """Get user data based on provided email address.""" - time.sleep(rate_limit_delay) - fields = ["real_name", "nick", "email", "name", "id"] - response = self._get( - "user", {"names": email, "include_fields": ",".join(fields)} - ) - if response.status_code != 200: - logger.error(f"Error searching {email}: {response.status_code}") - return - - # TODO: handle case when there is more than one match. - # Since possibly users can sneak in their email address in another field? - # Although we should validate the "email" field in BMO response + check for - # verification. - - result = response.json() - return result["users"][0] - - def get_user_by_id(self, id_: str = "", rate_limit_delay: float = 0.3): - """Get user data by BMO ID.""" - time.sleep(rate_limit_delay) - fields = ["real_name", "nick", "name", "id"] - response = self._get("user", {"ids": id_, "include_fields": ",".join(fields)}) - if response.status_code != 200: - logger.error(f"Error searching {id_}: {response.status_code}") - return - - result = response.json() - return result["users"][0] - - def get_users_by_ids(self, ids: list[str], rate_limit_delay: float = 0.3): - """Get user data by BMO ID.""" - time.sleep(rate_limit_delay) + def get_users_by_ids(self, ids: list[str]): + """Get user data by BMO IDs.""" fields = ["real_name", "nick", "name", "id", "email"] response = self._get("user", {"ids": ids, "include_fields": ",".join(fields)}) if response.status_code != 200: @@ -99,10 +68,3 @@ def get_match(self, match: str, rate_limit_delay: float = 0.3): else: return None return result["users"][0] - - # NOTES: - # How to store people? - # 64523: glob -> if nick is available - # 65182: Jane Smith -> if nick is not available, use provided name. - # OR - # - Masayuki Nakano bmo:19563 diff --git a/src/mots/cli.py b/src/mots/cli.py index 2daa395..1ff7283 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -22,7 +22,6 @@ import argparse from datetime import datetime import logging -import sys from pathlib import Path from mots import module @@ -36,84 +35,79 @@ logger = logging.getLogger(__name__) -class CLI: - """Wrap methods and call with the correct arguments.""" - - @staticmethod - def init(args): - """Call `file_config.init` with correct arguments.""" - file_config = FileConfig(Path(args.path)) - file_config.init() - - @staticmethod - def ls(args): - """Call `module.ls` with correct arguments.""" - file_config = FileConfig(Path(args.path)) - file_config.load() - return module.ls(file_config.config["modules"]) - - @staticmethod - def show(args): - """Call `module.show` with correct arguments.""" - file_config = FileConfig(Path(args.path)) - file_config.load() - return module.show(file_config.config["modules"], args.module) - - @staticmethod - def validate(args): - """Call `module.validate` with correct arguments.""" - file_config = FileConfig(Path(args.path)) - file_config.load() - return module.validate(file_config.config, args.repo_path) - - @staticmethod - def clean(args): - """Call `module.clean` with correct arguments.""" - file_config = FileConfig(Path(args.path)) - file_config.load() - return module.clean(file_config) - - @staticmethod - def query(args): - """Call `directory.query` with correct arguments.""" - file_config = FileConfig(Path(args.path)) - file_config.load() - directory = Directory(file_config) - directory.load() - result = directory.query(*args.paths) - for path in result.path_map: - modules = result.path_map[path] - module_names = ",".join([m.machine_name for m in modules]) - sys.stdout.write(f"{path}:{module_names}\n") - - @staticmethod - def add(args): - """Prompt user to add a new module.""" - params = { - "machine_name": input("Enter machine name of new module: "), - "name": input("Enter a human readable name: "), - "owners": get_list_input("Enter a comma separated list of owners"), - "peers": get_list_input("Enter a comma separated list of peers"), - "includes": get_list_input( - "Enter a comma separated list of paths to include" - ), - "excludes": get_list_input( - "Enter a comma separated list of paths to exclude" - ), - } - parent = input("Enter a machine name of the parent module (optional): ") or None - file_config = FileConfig(Path(args.path)) - module.add(params, file_config, parent=parent, write=True) - - @staticmethod - def export(args): - """Call `directory.export` with relevant parameters.""" - file_config = FileConfig(Path(args.path)) - file_config.load() - directory = Directory(file_config) - directory.load() - output = directory.export(args.format) - print(output) +def init(args: argparse.Namespace) -> None: + """Call `file_config.init` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.init() + + +def ls(args: argparse.Namespace) -> None: + """Call `module.ls` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + module.ls(file_config.config["modules"]) + + +def show(args: argparse.Namespace) -> None: + """Call `module.show` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + module.show(file_config.config["modules"], args.module) + + +def validate(args: argparse.Namespace) -> None: + """Call `module.validate` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + module.validate(file_config.config, args.repo_path) + + +def clean(args: argparse.Namespace) -> None: + """Call `module.clean` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + module.clean(file_config) + + +def query(args: argparse.Namespace) -> None: + """Call `directory.query` with correct arguments.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + directory = Directory(file_config) + directory.load() + result = directory.query(*args.paths) + for path in result.path_map: + modules = result.path_map[path] + module_names = ",".join([m.machine_name for m in modules]) + print(f"{path}:{module_names}\n") + + +def add(args: argparse.Namespace) -> None: + """Prompt user to add a new module.""" + params = { + "machine_name": input( + "Enter a machine name (alphanumeric characters and underscores)" + " for the new module: " + ), + "name": input("Enter a human readable name: "), + "owners": get_list_input("Enter a comma separated list of owners"), + "peers": get_list_input("Enter a comma separated list of peers"), + "includes": get_list_input("Enter a comma separated list of paths to include"), + "excludes": get_list_input("Enter a comma separated list of paths to exclude"), + } + parent = input("Enter a machine name of the parent module (optional): ") or None + file_config = FileConfig(Path(args.path)) + module.add(params, file_config, parent=parent, write=True) + + +def export(args: argparse.Namespace) -> None: + """Call `directory.export` with relevant parameters.""" + file_config = FileConfig(Path(args.path)) + file_config.load() + directory = Directory(file_config) + directory.load() + output = directory.export(args.format) + print(output) def main(): @@ -152,7 +146,7 @@ def create_parser(): help="the path of the repo to initialize", default=DEFAULT_CONFIG_FILEPATH, ) - init_parser.set_defaults(func=CLI.init) + init_parser.set_defaults(func=init) module_parser = subparsers.add_parser("module", help="module_operations") module_parser.add_argument( @@ -165,14 +159,14 @@ def create_parser(): module_parsers = module_parser.add_subparsers(title="module") list_parser = module_parsers.add_parser("list", help="list all modules") - list_parser.set_defaults(func=CLI.ls) + list_parser.set_defaults(func=ls) add_parser = module_parsers.add_parser("add", help="add a new module") - add_parser.set_defaults(func=CLI.add) + add_parser.set_defaults(func=add) show_parser = module_parsers.add_parser("show", help="show a module") show_parser.add_argument("module", help="name of the module to show") - show_parser.set_defaults(func=CLI.show) + show_parser.set_defaults(func=show) validate_parser = subparsers.add_parser( "validate", help="validate mots config for current repo" @@ -191,7 +185,7 @@ def create_parser(): help="the path of the repo", default=".", ) - validate_parser.set_defaults(func=CLI.validate) + validate_parser.set_defaults(func=validate) clean_parser = subparsers.add_parser( "clean", help="clean mots config for current repo" @@ -210,7 +204,7 @@ def create_parser(): help="the path of the repo", default=".", ) - clean_parser.set_defaults(func=CLI.clean) + clean_parser.set_defaults(func=clean) query_parser = subparsers.add_parser("query", help="query the module directory") query_parser.add_argument( @@ -221,7 +215,7 @@ def create_parser(): default=DEFAULT_CONFIG_FILEPATH, ) query_parser.add_argument("paths", nargs="+", help="a list of paths to query") - query_parser.set_defaults(func=CLI.query) + query_parser.set_defaults(func=query) export_parser = subparsers.add_parser("export", help="export the module directory") export_parser.add_argument( @@ -234,6 +228,6 @@ def create_parser(): export_parser.add_argument( "--format", "-f", type=str, default="wiki", help="the format of exported data" ) - export_parser.set_defaults(func=CLI.export) + export_parser.set_defaults(func=export) return parser, subparsers diff --git a/src/mots/directory.py b/src/mots/directory.py index 0f64324..4d3a4e9 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -240,8 +240,7 @@ def __init__(self, people, query_bmo: bool = True): people = list(people) self.people = [] self.by_bmo_id = {} - for i in range(len(people)): - p = people[i] + for i, p in enumerate(people): logger.debug(f"Adding person {p} to roster...") self.people.append( Person( diff --git a/src/mots/module.py b/src/mots/module.py index 481eb28..4702e0e 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -8,8 +8,8 @@ from collections import defaultdict import logging from pathlib import Path -from mots.utils import generate_machine_readable_name from mots.config import FileConfig +from mots.utils import generate_machine_readable_name logger = logging.getLogger(__name__) @@ -147,7 +147,7 @@ def serialize(self): return serialized - def validate(self, errors=None): + def validate(self): """Perform validation on module and submodules recursively. Starting with the current module, ensure that this module includes at least one @@ -157,7 +157,7 @@ def validate(self, errors=None): :param errors: a list of errors to append to :rtype: list """ - errors = errors or [] + errors = [] if not self.machine_name.strip(): errors.append("Module has a blank machine_name.") @@ -168,23 +168,11 @@ def validate(self, errors=None): if not self.calculate_paths(): errors.append(f"No valid paths were found in {self.machine_name}.") - # # TODO do this validation against BMO/mock client - # if self.owners: - # for owner in self.owners: - # logger.debug(f"Parsing owner {owner}...") - # owner = parse_user_string(owner) - # if not owner["email"]: - # errors.append(f"No valid email found for owner {owner['name']}") - # if self.peers: - # for peer in self.peers: - # logger.debug(f"Parsing peer {peer}...") - # peer = parse_user_string(peer) - # if not peer["email"]: - # errors.append(f"No valid email found for peer {peer['name']}") + # TODO: validate people if self.submodules: for submodule in self.submodules: - return submodule.validate(errors=errors) + errors += submodule.validate() return errors @@ -205,24 +193,11 @@ def show(modules: list[Module], module: str): :raises ValueError: when no module matches the given machine name """ - modules_by_name = {} for _module in modules: - modules_by_name[_module["machine_name"]] = _module - if module not in modules_by_name: - raise ValueError(f"{module} not found.") - else: - print(modules_by_name[module]) - - -def extract_people(module): - """Return a list of people that are in a module or submodule.""" - people_keys = ["owners", "peers"] # "owners_emeritus", "peers_emeritus" - people = [] - for key in people_keys: - if key in module and module[key]: - logger.debug(f"Extracting people from {module[key]} ({key})") - people += module[key] - return people + if _module["machine_name"] == module: + print(_module) + return + raise ValueError(f"{module} not found.") def clean(file_config: FileConfig, write: bool = True): diff --git a/src/mots/pmo.py b/src/mots/pmo.py index 9a8c181..7dcbfcc 100644 --- a/src/mots/pmo.py +++ b/src/mots/pmo.py @@ -1,3 +1,7 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + """Module that provides helpers to interact with the PMO API.""" import requests diff --git a/tests/conftest.py b/tests/conftest.py index 271759c..2bc7ba6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,6 +80,7 @@ def config(): ], "excludes": ["canines/red_fox"], "owners": [people[0]], + "peers": [people[1]], "submodules": [ { "machine_name": "predators", @@ -110,6 +111,7 @@ def config(): "birds/eagle", ], "owners": [people[2]], + "peers": [people[1]], }, ], } diff --git a/tests/test_directory.py b/tests/test_directory.py index 81d617a..aac2ee7 100644 --- a/tests/test_directory.py +++ b/tests/test_directory.py @@ -105,7 +105,7 @@ def test_directory__Directory__query(repo): Person(bmo_id=2, name="otis", info="testing", nick="otis") } assert set(result.peers) == { - Person(bmo_id=2, name="otis", info="testing", nick="otis") + Person(bmo_id=1, name="jill", info="testing", nick="jill") } assert result.rejected_paths == ["felines/maine_coon"] @@ -144,6 +144,6 @@ def test_directory__Directory__query_merging(repo): Person(bmo_id=2, name="otis", info="testing", nick="otis") } assert set(result.peers) == { - Person(bmo_id=2, name="otis", info="testing", nick="otis") + Person(bmo_id=1, name="jill", info="testing", nick="jill") } assert result.rejected_paths == ["felines/maine_coon"] diff --git a/tests/test_module.py b/tests/test_module.py index 2483c2f..1c9ac4b 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -66,12 +66,20 @@ def test_module__Module__validate__error_no_paths_in_submodule(repo): name="Submodule", machine_name="submodule", excludes="*", - ) + ), + dict( + name="Submodule2", + machine_name="submodule2", + excludes="*", + ), ], ) m = Module(**m, repo_path=str(repo)) errors = m.validate() - assert errors == ["No valid paths were found in submodule."] + assert errors == [ + "No valid paths were found in submodule.", + "No valid paths were found in submodule2.", + ] def test_module__Module__validate__invalid_machine_name(repo): From c08f758a9728298a461dc5487446249cf539c9b0 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Wed, 2 Feb 2022 09:14:31 -0500 Subject: [PATCH 13/26] Additional improvements: - add export to rst functionality; deprecate wiki export --- setup.cfg | 4 ++ src/mots/cli.py | 7 +-- src/mots/export.py | 47 +++++++++++++++++ src/mots/module.py | 3 ++ src/mots/templates/directory.template.rst | 62 +++++++++++++++++++++++ tests/test_module.py | 6 ++- 6 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 src/mots/export.py create mode 100644 src/mots/templates/directory.template.rst diff --git a/setup.cfg b/setup.cfg index 7fc8e34..5d02850 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ packages = find: python_requires = >=3.7 install_requires = ruamel.yaml==0.17.16 + importlib_resources; python_version < "3.9" [options.packages.find] where = src @@ -32,3 +33,6 @@ where = src [options.entry_points] console_scripts = mots = mots.cli:main + +[options.package_data] +* = *.template.rst diff --git a/src/mots/cli.py b/src/mots/cli.py index 1ff7283..8b37e5c 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -27,6 +27,7 @@ from mots import module from mots.config import FileConfig from mots.directory import Directory +from mots.export import export_to_format from mots.logging import init_logging from mots.config import DEFAULT_CONFIG_FILEPATH @@ -101,12 +102,12 @@ def add(args: argparse.Namespace) -> None: def export(args: argparse.Namespace) -> None: - """Call `directory.export` with relevant parameters.""" + """Call `export.export_to_format` with relevant parameters.""" file_config = FileConfig(Path(args.path)) file_config.load() directory = Directory(file_config) directory.load() - output = directory.export(args.format) + output = export_to_format(directory, args.format) print(output) @@ -226,7 +227,7 @@ def create_parser(): default=DEFAULT_CONFIG_FILEPATH, ) export_parser.add_argument( - "--format", "-f", type=str, default="wiki", help="the format of exported data" + "--format", "-f", type=str, default="rst", help="the format of exported data" ) export_parser.set_defaults(func=export) diff --git a/src/mots/export.py b/src/mots/export.py new file mode 100644 index 0000000..34d3af7 --- /dev/null +++ b/src/mots/export.py @@ -0,0 +1,47 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +"""Export directory to various formats.""" + +from __future__ import annotations +import logging + +import sys + +if sys.version_info < (3, 9): + import importlib_resources +else: + import importlib.resources as importlib_resources + +import jinja2 + +from mots.directory import Directory + +logger = logging.getLogger(__name__) + + +class Exporter: + """A helper class that exports to various formats.""" + + def __init__(self, directory: Directory): + self.directory = directory + + def _export_to_rst(self): + loader = jinja2.FileSystemLoader( + searchpath=importlib_resources.files("mots") / "templates" + ) + env = jinja2.Environment(loader=loader) + template = env.get_template("directory.template.rst") + out = template.render(directory=self.directory) + return out + + +def export_to_format(directory: Directory, frmt="rst"): + """Export directory in a specified format.""" + supported_formats = ["rst"] + if frmt not in supported_formats: + raise ValueError(f"{frmt} not one of {supported_formats}.") + + exporter = Exporter(directory) + return getattr(exporter, f"_export_to_{frmt}")() diff --git a/src/mots/module.py b/src/mots/module.py index 4702e0e..6595411 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -96,6 +96,9 @@ def __init__( if not self.peers and self.parent: self.peers = self.parent.peers + self.owner_names = [owner["nick"] for owner in self.owners] + self.peer_names = [peer["nick"] for peer in self.peers] + def calculate_paths(self): """Calculate paths based on inclusions and exclusions. diff --git a/src/mots/templates/directory.template.rst b/src/mots/templates/directory.template.rst new file mode 100644 index 0000000..ad381e6 --- /dev/null +++ b/src/mots/templates/directory.template.rst @@ -0,0 +1,62 @@ +{# This Source Code Form is subject to the terms of the Mozilla Public #} +{# License, v. 2.0. If a copy of the MPL was not distributed with this #} +{# file, You can obtain one at https://mozilla.org/MPL/2.0/. #} + +{%- macro module_entry(module, is_submodule=False) -%} +{{ module.name }} +{{ "~" * module.name|length if not is_submodule else "=" * module.name|length }} + +{{ module.description }} + +{% if not module.owners %} +.. warning:: + + This module does not have any owners specified. +{% endif %} + +.. list-table:: + :stub-columns: 1 + +{% if module.owners %} + * - Owner(s) + - {{ module.owner_names|join(", ") }} +{% endif %} +{% if module.peers%} + * - Peer(s) + - {{ module.peer_names|join(", ") }} +{% endif %} +{% if module.includes %} + * - Includes + - {{ module.includes|join(", ")|replace("*", "\*") }} +{% endif %} +{% if module.excludes %} + * - Excludes + - {{ module.excludes|join(", ")|replace("*", "\*") }} +{% endif %} +{% if module.meta.group %} + * - Group + - {{ module.meta.group }} +{% endif %} +{% if module.meta.url %} + * - URL + - {{ module.meta.url }} +{% endif %} +{% if module.meta.components %} + * - Bugzilla Components + - {{ module.meta.components|join(", ") }} +{% endif %} +{% endmacro %} + +======= +Modules +======= +{{ directory.description }} + +{%- for module in directory.modules -%} +{{ module_entry(module) }} +{% if module.submodules %} +{% for submodule in module.submodules %} +{{ module_entry(submodule, True) }} +{% endfor %} +{% endif %} +{% endfor %} diff --git a/tests/test_module.py b/tests/test_module.py index 1c9ac4b..87edf22 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -157,7 +157,7 @@ def test_module__add(repo): module = { "machine_name": "reptiles", "includes": ["reptiles/**/*"], - "owners": ["angel"], + "owners": [{"nick": "otis", "bmo_id": 2, "info": "testing"}], } add(module, file_config, parent="pets", write=True) @@ -170,7 +170,9 @@ def test_module__add(repo): assert file_config.config["modules"][1]["submodules"][0]["includes"] == [ "reptiles/**/*" ] - assert file_config.config["modules"][1]["submodules"][0]["owners"] == ["angel"] + assert file_config.config["modules"][1]["submodules"][0]["owners"] == [ + {"nick": "otis", "bmo_id": 2, "info": "testing"} + ] # TODO: validate that includes, excludes, and owners are lists... From efcb8f5f9cb6f71a16cc1f01c648e829dc4a0aa5 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Wed, 2 Feb 2022 14:08:39 -0500 Subject: [PATCH 14/26] Code review feedback + bug fixes - Fixes bug 1750200 (yaml_set_anchor issue) --- CODE_OF_CONDUCT.md | 8 +++ README.md | 26 ++++++- documentation/index.rst | 60 +--------------- setup.cfg | 2 +- src/mots/bmo.py | 7 +- src/mots/cli.py | 28 ++++---- src/mots/config.py | 149 +++++++++++++++++++++++++++++++++++++++- src/mots/directory.py | 99 ++++++++------------------ src/mots/module.py | 146 --------------------------------------- src/mots/pmo.py | 8 ++- tests/test_module.py | 9 +-- 11 files changed, 234 insertions(+), 308 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f0ef43f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,8 @@ +# Community Participation Guidelines + +This repository is governed by Mozilla's code of conduct and etiquette guidelines. +For more details, please read the +[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). + +## How to Report +For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. diff --git a/README.md b/README.md index 8427800..ca3c257 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -For full documentation, see https://mots.readthedocs.io/en/develop/. +mots - Module Ownership in Tree System +====================================== + +[Full documentation](https://mots.readthedocs.io/en/develop/) can be viewed online. + +### Contributing + +- All contributors must abide by the Mozilla Code of Conduct. + +- The [main repository](https://github.com/mozilla-conduit/mots) is hosted on GitHub. Pull requests should be submitted against the `main` branch. + +- Bugs are tracked [on Bugzilla](https://bugzilla.mozilla.org), under the `Conduit::mots` component. + + +Development environment +======================= +To set up a local development environment, run the following commands. Optionally replace the python version with the desired version on your local machine. + +.. code-block:: bash + + make dev-env PY=python3.9 + source .mots-env/bin/activate + make dev + +The above commands will set up a local development environment using the provided python version available on your machine, and subsequently install all required packages in that environment. diff --git a/documentation/index.rst b/documentation/index.rst index d87de4c..40e48a1 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -37,66 +37,8 @@ Utils Command Line Usage ================== -You can get this help by typing ``mots`` at the command line. +You can get command line usage help by typing ``mots --help`` at the command line. -usage: mots [-h] [--debug] {init,module,validate,clean,query} ... - -main command line interface for mots - -optional arguments: - -h, --help show this help message and exit - --debug enable debug output - -commands: - {init,module,validate,clean,query} - init initialize mots configuration in repo - module module_operations - validate validate mots config for current repo - clean clean mots config for current repo - query query the module directory - -usage: mots init [-h] [--path PATH] - -optional arguments: - -h, --help show this help message and exit - --path PATH, -p PATH the path of the repo to initialize - -usage: mots module [-h] [--path PATH] {list,add,show} ... - -optional arguments: - -h, --help show this help message and exit - --path PATH, -p PATH the path of the repo config file - -module: - {list,add,show} - list list all modules - add add a new module - show show a module - -usage: mots validate [-h] [--path PATH] [--repo-path REPO_PATH] - -optional arguments: - -h, --help show this help message and exit - --path PATH, -p PATH the path of the repo config file - --repo-path REPO_PATH, -r REPO_PATH - the path of the repo - -usage: mots clean [-h] [--path PATH] [--repo-path REPO_PATH] - -optional arguments: - -h, --help show this help message and exit - --path PATH, -p PATH the path of the repo config file - --repo-path REPO_PATH, -r REPO_PATH - the path of the repo - -usage: mots query [-h] [--path PATH] paths [paths ...] - -positional arguments: - paths a list of paths to query - -optional arguments: - -h, --help show this help message and exit - --path PATH, -p PATH the path of the repo config file Development environment ======================= diff --git a/setup.cfg b/setup.cfg index 5d02850..db48d96 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ version = 0.0.1.a0 author = Zeid Zabaneh author_email = zeid@mozilla.com description = Module Ownership in Tree System -long_description = file: README.md +long_description = "Full documentation can be found at https://mots.readthedocs.io/en/develop/." long_description_content_type = text/markdown url = https://github.com/mozilla-conduit/mots classifiers = diff --git a/src/mots/bmo.py b/src/mots/bmo.py index 000f9d8..dae1bde 100644 --- a/src/mots/bmo.py +++ b/src/mots/bmo.py @@ -8,7 +8,6 @@ import requests import os import logging -import time logger = logging.getLogger(__name__) @@ -51,9 +50,8 @@ def get_users_by_ids(self, ids: list[str]): result = response.json() return {u["id"]: u for u in result["users"]} - def get_match(self, match: str, rate_limit_delay: float = 0.3): + def get_match(self, match: str): """Get user data based on provided info.""" - time.sleep(rate_limit_delay) fields = ["real_name", "nick", "email", "name", "id"] response = self._get( "user", {"match": match, "include_fields": ",".join(fields)} @@ -65,6 +63,3 @@ def get_match(self, match: str, rate_limit_delay: float = 0.3): result = response.json() if result["users"]: return result["users"][0] - else: - return None - return result["users"][0] diff --git a/src/mots/cli.py b/src/mots/cli.py index 8b37e5c..734486f 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -25,7 +25,7 @@ from pathlib import Path from mots import module -from mots.config import FileConfig +from mots import config from mots.directory import Directory from mots.export import export_to_format from mots.logging import init_logging @@ -38,41 +38,41 @@ def init(args: argparse.Namespace) -> None: """Call `file_config.init` with correct arguments.""" - file_config = FileConfig(Path(args.path)) + file_config = config.FileConfig(Path(args.path)) file_config.init() def ls(args: argparse.Namespace) -> None: """Call `module.ls` with correct arguments.""" - file_config = FileConfig(Path(args.path)) + file_config = config.FileConfig(Path(args.path)) file_config.load() module.ls(file_config.config["modules"]) def show(args: argparse.Namespace) -> None: """Call `module.show` with correct arguments.""" - file_config = FileConfig(Path(args.path)) + file_config = config.FileConfig(Path(args.path)) file_config.load() module.show(file_config.config["modules"], args.module) def validate(args: argparse.Namespace) -> None: - """Call `module.validate` with correct arguments.""" - file_config = FileConfig(Path(args.path)) + """Call `config.validate` with correct arguments.""" + file_config = config.FileConfig(Path(args.path)) file_config.load() - module.validate(file_config.config, args.repo_path) + config.validate(file_config.config, args.repo_path) def clean(args: argparse.Namespace) -> None: - """Call `module.clean` with correct arguments.""" - file_config = FileConfig(Path(args.path)) + """Call `config.clean` with correct arguments.""" + file_config = config.FileConfig(Path(args.path)) file_config.load() - module.clean(file_config) + config.clean(file_config) def query(args: argparse.Namespace) -> None: """Call `directory.query` with correct arguments.""" - file_config = FileConfig(Path(args.path)) + file_config = config.FileConfig(Path(args.path)) file_config.load() directory = Directory(file_config) directory.load() @@ -97,13 +97,13 @@ def add(args: argparse.Namespace) -> None: "excludes": get_list_input("Enter a comma separated list of paths to exclude"), } parent = input("Enter a machine name of the parent module (optional): ") or None - file_config = FileConfig(Path(args.path)) - module.add(params, file_config, parent=parent, write=True) + file_config = config.FileConfig(Path(args.path)) + config.add(params, file_config, parent=parent, write=True) def export(args: argparse.Namespace) -> None: """Call `export.export_to_format` with relevant parameters.""" - file_config = FileConfig(Path(args.path)) + file_config = config.FileConfig(Path(args.path)) file_config.load() directory = Directory(file_config) directory.load() diff --git a/src/mots/config.py b/src/mots/config.py index ab8b74e..842472b 100644 --- a/src/mots/config.py +++ b/src/mots/config.py @@ -3,12 +3,18 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. """Configuration classes used to initialize and manage mots in a repo.""" + +from collections import defaultdict import logging from datetime import datetime from pathlib import Path from ruamel.yaml import YAML +from mots.directory import Directory +from mots.module import Module +from mots.utils import generate_machine_readable_name + logger = logging.getLogger(__name__) yaml = YAML() @@ -17,6 +23,12 @@ DEFAULT_CONFIG_FILEPATH = Path("./mots.yaml") +class ValidationError(TypeError): + """Thrown when a particular module is not valid.""" + + pass + + class FileConfig: """Loader and writer for filesystem based configuration.""" @@ -46,11 +58,144 @@ def init(self): def load(self): """Load configuration from file.""" - with open(self.path, "r") as f: + with self.path.open("r") as f: self.config = yaml.load(f) def write(self): """Write configuration to file, and update the timestamp.""" self.config["updated_at"] = datetime.now().isoformat() - with open(self.path, "w") as f: + with self.path.open("w") as f: yaml.dump(self.config, f) + + +def clean(file_config: FileConfig, write: bool = True): + """Clean and re-sort configuration file. + + Load configuration from disk, sort modules and submodules by `machine_name`. If + there is no valid `machine_name`, generate one. Write changes to disk. + + :param file_config: an instance of :class:`FileConfig` + :param write: if set to `True`, writes changes to disk. + """ + file_config.load() + directory = Directory(file_config) + directory.load() + for i, module in enumerate(file_config.config["modules"]): + if "machine_name" not in module: + module["machine_name"] = generate_machine_readable_name(module["name"]) + + people_keys = ("owners", "peers") + for key in people_keys: + if key in module and module[key]: + for i, person in enumerate(module[key]): + module[key][i] = file_config.config["people"][ + directory.people.by_bmo_id[person["bmo_id"]] + ] + + # Do the same for submodules. + if "submodules" in module and module["submodules"]: + module["submodules"].sort(key=lambda x: x["name"]) + for submodule in module["submodules"]: + for key in people_keys: + if key in submodule and submodule[key]: + for i, person in enumerate(submodule[key]): + person = submodule[key][i] + submodule[key][i] = file_config.config["people"][ + directory.people.by_bmo_id[person["bmo_id"]] + ] + if "machine_name" not in submodule: + submodule["machine_name"] = generate_machine_readable_name( + submodule["name"] + ) + + file_config.config["modules"].sort(key=lambda x: x["machine_name"]) + if write: + # Write all changes. + file_config.write() + + # Reload changes from disk so we can reset all anchors. + file_config.load() + + nicks = [] + for person in file_config.config["people"]: + machine_readable_nick = generate_machine_readable_name( + person["nick"], keep_case=True + ) + if machine_readable_nick in nicks or not machine_readable_nick: + continue + nicks.append(machine_readable_nick) + person.yaml_set_anchor(machine_readable_nick) + + file_config.write() + + +def validate(config: dict, repo_path: str): + """Validate the current state of the config file. + + - Check if top-level dictionary contains required keys + - Check if machine names are unique + - Instantiate and run validation on each :class:`Module` instance + + :raises ValidationError: if any validation errors are detected + """ + # Validate that config has all the required keys. + required_keys = ("repo", "created_at", "updated_at", "modules") + keys_diff = required_keys - config.keys() + if len(keys_diff) != 0: + raise ValidationError(f"{keys_diff} missing from configuration file.") + + modules = config["modules"] + + # Count machine name repetitions in a defaultdict. + machine_names = defaultdict(int) + for m in modules: + machine_names[m["machine_name"]] += 1 + if "submodules" in m and m["submodules"]: + for sm in m["submodules"]: + machine_names[sm["machine_name"]] += 1 + + machine_names = {name: count for name, count in machine_names.items() if count > 1} + if machine_names: + raise ValidationError( + f"Duplicate machine name(s) found: {', '.join(machine_names.keys())}" + ) + + errors = [] + for i, module in enumerate(config["modules"]): + module = Module(repo_path=repo_path, **module) + validation_errors = module.validate() + if validation_errors: + errors.append(validation_errors) + + if errors: + raise ValidationError(errors) + logger.info("All modules validated successfully.") + + +def add( + new_module: dict, file_config: FileConfig, parent: str = None, write: bool = True +): + """Add a new module to the configuration. + + :param module: a dictionary containing module parameters + :param file_config: an instance of :class:`FileConfig` + :param parent: the machine name of the parent module if applicable + :param write: if set to `True`, writes changes to disk + """ + file_config.load() + modules = file_config.config["modules"] + serialized = Module(**new_module, repo_path=file_config.repo_path).serialize() + + if parent: + for module in modules: + if module["machine_name"] == parent: + if "submodules" not in module or not module["submodules"]: + module["submodules"] = [] + module["submodules"].append(serialized) + break + else: + modules.append(serialized) + # TODO: do better validation here, via schema validation or otherwise + + if write: + file_config.write() diff --git a/src/mots/directory.py b/src/mots/directory.py index 4d3a4e9..2697dd4 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -12,49 +12,25 @@ import logging from mots.bmo import BMOClient from mots.module import Module -from mots.config import FileConfig from mots.utils import parse_real_name +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from mots.config import FileConfig + + logger = logging.getLogger(__name__) -WIKI_TMPL = """ -{% macro people_entry(people) %} -{%- for p in people %}{{ p.nick }}{% if not loop.last %}, {% endif %}{% endfor %} -{% endmacro %} - -{%- macro module_entry(module) -%} -{% raw %}{{Module{% endraw %} -|name={{ module.name }} -|description={{ module.description }} -|owner(s)={{ people_entry(module.owners) -}} -|peer(s)={{ people_entry(module.peers) -}} -|includes={{ module.includes|join(", ") }} -|excludes={{ module.excludes|join(", ") }} -{%- if module.meta.group -%} -|group={{ module.meta.group }} -{% endif %} -{%- if module.meta.url -%} -|url={{ module.meta.group }} -{% endif %} -{%- if module.meta.components -%} -|url={{ module.meta.components }} -{% endif %} -{% raw %}}}{% endraw %} -{% endmacro %} - - -{{ directory.description or "No directory description provided." }} - -{%- for module in directory.modules -%} -{{ module_entry(module) }} -{% if module.submodules %} -== Submodules == -{% for submodule in module.submodules %} -{{ module_entry(submodule) }} -{% endfor %} -{% endif %} -{% endfor %} -""" + +def _get_bmo_data(people: list) -> dict: + """Fetch an updated dictionary from Bugzilla with user data. + + Dictionary keys are set to user IDs, and values are set to various data. + """ + bmo_client = BMOClient() + bmo_data = bmo_client.get_users_by_ids([p["bmo_id"] for p in people]) + return bmo_data class Directory: @@ -110,7 +86,9 @@ def load(self, full_paths: bool = False, query_bmo=True): self.index = dict(self.index) # Load people directory - self.people = People(self.config_handle.config["people"], query_bmo=query_bmo) + people = list(self.config_handle.config["people"]) + bmo_data = _get_bmo_data(people) if query_bmo else {} + self.people = People(people, bmo_data) if self.people.serialized != list(self.config_handle.config["people"]): logger.debug("People directory modified, updating configuration...") self.config_handle.config["people"] = self.people.serialized @@ -129,21 +107,6 @@ def query(self, *paths: str) -> QueryResult: return QueryResult(result, rejected) - def _export_wiki(self): - from jinja2 import Template - - template = Template(WIKI_TMPL) - out = template.render(directory=self) - return out - - def export(self, frmt="wiki"): - """Export directory in a specified format.""" - supported_formats = ["wiki"] - if frmt not in supported_formats: - raise ValueError(f"{frmt} not one of {supported_formats}.") - - return getattr(self, f"_export_{frmt}")() - class QueryResult: """Helper class to simplify query result interpretation.""" @@ -211,12 +174,14 @@ def __post_init__(self, bmo_data): """Refresh BMO data from BMO API.""" if bmo_data: self.nick = bmo_data.get("nick", "") - self.bmo_id = bmo_data.get("id") or self.bmo_id + self.bmo_id = bmo_data.get("id", self.bmo_id) real_name = bmo_data.get("real_name", "") parsed_real_name = parse_real_name(real_name) self.name = parsed_real_name["name"] self.info = parsed_real_name["info"] + else: + logger.warning(f"No bugzilla data was provided for user {self.bmo_id}") def __hash__(self): """Return a unique identifier for this person.""" @@ -230,24 +195,20 @@ class People: people: list = None serialized: list = None - def __init__(self, people, query_bmo: bool = True): + def __init__(self, people, bmo_data: dict): logger.debug(f"Initializing people directory with {len(people)} people...") - if query_bmo: - bmo_client = BMOClient() - bmo_data = bmo_client.get_users_by_ids([p["bmo_id"] for p in people]) - else: - bmo_data = {} people = list(people) self.people = [] self.by_bmo_id = {} - for i, p in enumerate(people): - logger.debug(f"Adding person {p} to roster...") + for i, person in enumerate(people): + logger.debug(f"Adding person {person} to roster...") + # TODO: should have a fallback here without BMO data. self.people.append( Person( - bmo_id=p["bmo_id"], - bmo_data=bmo_data.get(p["bmo_id"]), + bmo_id=person["bmo_id"], + bmo_data=bmo_data.get(person["bmo_id"]), ) ) - self.by_bmo_id[p["bmo_id"]] = i - logger.debug(f"Person {p} added to position {i}.") - self.serialized = [asdict(p) for p in self.people] + self.by_bmo_id[person["bmo_id"]] = i + logger.debug(f"Person {person} added to position {i}.") + self.serialized = [asdict(person) for person in self.people] diff --git a/src/mots/module.py b/src/mots/module.py index 6595411..d9094d7 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -5,21 +5,12 @@ """Module operations and utility functions.""" from __future__ import annotations -from collections import defaultdict import logging from pathlib import Path -from mots.config import FileConfig -from mots.utils import generate_machine_readable_name logger = logging.getLogger(__name__) -class ValidationError(TypeError): - """Thrown when a particular module is not valid.""" - - pass - - class Module: """A top-level module or a submodule. @@ -201,140 +192,3 @@ def show(modules: list[Module], module: str): print(_module) return raise ValueError(f"{module} not found.") - - -def clean(file_config: FileConfig, write: bool = True): - """Clean and re-sort configuration file. - - Load condiguration from disk, sort modules and submodules by `machine_name`. If - there is no valid `machine_name`, generate one. Write changes to disk. - - :param file_config: an instance of :class:`FileConfig` - :param write: if set to `True`, writes changes to disk. - """ - from mots.directory import Directory - - file_config.load() - directory = Directory(file_config) - directory.load() - for i in range(len(file_config.config["modules"])): - module = file_config.config["modules"][i] - if "machine_name" not in module: - module["machine_name"] = generate_machine_readable_name(module["name"]) - - people_keys = ("owners", "peers") - for key in people_keys: - if key in module and module[key]: - for i in range(len(module[key])): - person = module[key][i] - module[key][i] = file_config.config["people"][ - directory.people.by_bmo_id[person["bmo_id"]] - ] - - # Do the same for submodules. - if "submodules" in module and module["submodules"]: - module["submodules"].sort(key=lambda x: x["name"]) - for submodule in module["submodules"]: - for key in people_keys: - if key in submodule and submodule[key]: - for i in range(len(submodule[key])): - person = submodule[key][i] - submodule[key][i] = file_config.config["people"][ - directory.people.by_bmo_id[person["bmo_id"]] - ] - if "machine_name" not in submodule: - submodule["machine_name"] = generate_machine_readable_name( - submodule["name"] - ) - - file_config.config["modules"].sort(key=lambda x: x["machine_name"]) - - nicks = [] - for p in file_config.config["people"]: - machine_readable_nick = generate_machine_readable_name( - p["nick"], keep_case=True - ) - if machine_readable_nick in nicks or not machine_readable_nick: - continue - nicks.append(machine_readable_nick) - try: - p.yaml_set_anchor(machine_readable_nick) - except Exception as e: - # NOTE: this happens when the file changes and we lose ruamel embedded data - # TODO: fix this - logger.error(e) - - if write: - file_config.write() - - -def validate(config: dict, repo_path: str): - """Validate the current state of the config file. - - - Check if top-level dictionary contains required keys - - Check if machine names are unique - - Instantiate and run validation on each :class:`Module` instance - - :raises ValidationError: if any validation errors are detected - """ - # Validate that config has all the required keys. - required_keys = ("repo", "created_at", "updated_at", "modules") - keys_diff = required_keys - config.keys() - if len(keys_diff) != 0: - raise ValidationError(f"{keys_diff} missing from configuration file.") - - modules = config["modules"] - - # Count machine name repetitions in a defaultdict. - machine_names = defaultdict(int) - for m in modules: - machine_names[m["machine_name"]] += 1 - if "submodules" in m and m["submodules"]: - for sm in m["submodules"]: - machine_names[sm["machine_name"]] += 1 - - machine_names = {name: count for name, count in machine_names.items() if count > 1} - if machine_names: - raise ValidationError( - f"Duplicate machine name(s) found: {', '.join(machine_names.keys())}" - ) - - errors = [] - for i in range(len(config["modules"])): - module = config["modules"][i] - module = Module(repo_path=repo_path, **module) - validation_errors = module.validate() - if validation_errors: - errors.append(validation_errors) - - if errors: - raise ValidationError(errors) - logger.info("All modules validated successfully.") - - -def add( - new_module: dict, file_config: FileConfig, parent: str = None, write: bool = True -): - """Add a new module to the configuration. - - :param module: a dictionary containing module parameters - :param file_config: an instance of :class:`FileConfig` - :param parent: the machine name of the parent module if applicable - :param write: if set to `True`, writes changes to disk - """ - file_config.load() - modules = file_config.config["modules"] - serialized = Module(**new_module, repo_path=file_config.repo_path).serialize() - - if parent: - for module in modules: - if module["machine_name"] == parent: - if "submodules" not in module or not module["submodules"]: - module["submodules"] = [] - module["submodules"].append(serialized) - break - else: - modules.append(serialized) - - if write: - file_config.write() diff --git a/src/mots/pmo.py b/src/mots/pmo.py index 7dcbfcc..f1d572d 100644 --- a/src/mots/pmo.py +++ b/src/mots/pmo.py @@ -13,6 +13,7 @@ DEFAULT_BASE_URL = "https://people.mozilla.org/api/v4/" USER_AGENT = "mots" # TODO: improve this and include version. +PMO_COOKIE_ENV_VAR = "PMO_COOKIE" class PMOClient: @@ -20,9 +21,12 @@ class PMOClient: def __init__(self, token: str = None, base_url: str = DEFAULT_BASE_URL): if not token: - token = os.getenv("PMO_COOKIE", input("Enter PMO Cookie: ")) + token = os.getenv("PMO_COOKIE_ENV_VAR") if not token: - raise Exception() + raise ValueError( + f"{PMO_COOKIE_ENV_VAR} environment variable missing," + " and no other token was explicitly provided" + ) self.headers = {"Cookie": token, "User-Agent": USER_AGENT} self.base_url = base_url diff --git a/tests/test_module.py b/tests/test_module.py index 87edf22..51b562e 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -7,11 +7,8 @@ from __future__ import annotations import pytest -from mots.config import FileConfig -from mots.module import add +from mots.config import FileConfig, add, validate, ValidationError from mots.module import Module -from mots.module import ValidationError -from mots.module import validate def test_module__Module(repo): @@ -175,7 +172,3 @@ def test_module__add(repo): ] # TODO: validate that includes, excludes, and owners are lists... - - -def test_module__clean(repo): - pass From b8fa8b53832428d44d19d8eeeab0b6098121b731 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Wed, 2 Feb 2022 14:35:38 -0500 Subject: [PATCH 15/26] update .gitignore --- .gitignore | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.gitignore b/.gitignore index 58595cf..f06614d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,3 @@ __pycache__ dist/ documentation/_build htmlcov/ -mots.log -notes -# should be removed later -dump -parse_wiki.py -wiki -wiki.yaml -src/mots/parking_lot.py From 857f82bee4f360bf4afd8b4937077f95da484107 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Wed, 2 Feb 2022 14:58:25 -0500 Subject: [PATCH 16/26] Bug fixes + dog fooding - Fix `mots module add` command so that it adds bmo_id key - Fix `mots init` command to include "people" key --- mots.yaml | 19 +++++++++++++++++++ src/mots/cli.py | 8 ++++++-- src/mots/config.py | 13 +++++++++---- src/mots/module.py | 4 ++-- 4 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 mots.yaml diff --git a/mots.yaml b/mots.yaml new file mode 100644 index 0000000..097ff00 --- /dev/null +++ b/mots.yaml @@ -0,0 +1,19 @@ +repo: mots +created_at: '2022-02-02T14:48:50.574414' +updated_at: '2022-02-02T14:57:03.916934' +modules: + - machine_name: main + name: Main + includes: + - '**/*' + excludes: [] + owners: + - &zeid + bmo_id: 633708 + name: Zeid Zabaneh + info: '[:zeid]' + nick: zeid + peers: [] + meta: +people: + - *zeid diff --git a/src/mots/cli.py b/src/mots/cli.py index 734486f..2ec7d9c 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -91,11 +91,15 @@ def add(args: argparse.Namespace) -> None: " for the new module: " ), "name": input("Enter a human readable name: "), - "owners": get_list_input("Enter a comma separated list of owners"), - "peers": get_list_input("Enter a comma separated list of peers"), + "owners": get_list_input("Enter a comma separated list of owner bugzilla IDs"), + "peers": get_list_input("Enter a comma separated list of peer bugzilla IDs"), "includes": get_list_input("Enter a comma separated list of paths to include"), "excludes": get_list_input("Enter a comma separated list of paths to exclude"), } + + params["owners"] = [{"bmo_id": int(bmo_id)} for bmo_id in params["owners"]] + params["peers"] = [{"bmo_id": int(bmo_id)} for bmo_id in params["peers"]] + parent = input("Enter a machine name of the parent module (optional): ") or None file_config = config.FileConfig(Path(args.path)) config.add(params, file_config, parent=parent, write=True) diff --git a/src/mots/config.py b/src/mots/config.py index 842472b..6d5b206 100644 --- a/src/mots/config.py +++ b/src/mots/config.py @@ -49,7 +49,8 @@ def init(self): "repo": str(Path(self.path).resolve().parts[-2]), "created_at": now, "updated_at": None, - "modules": None, + "modules": [], + "people": [], } self.write() logger.info(f"mots configuration initialized in {self.path}.") @@ -88,9 +89,13 @@ def clean(file_config: FileConfig, write: bool = True): for key in people_keys: if key in module and module[key]: for i, person in enumerate(module[key]): - module[key][i] = file_config.config["people"][ - directory.people.by_bmo_id[person["bmo_id"]] - ] + try: + module[key][i] = file_config.config["people"][ + directory.people.by_bmo_id[person["bmo_id"]] + ] + except KeyError: + file_config.config["people"].append(person) + module[key][i] = person # Do the same for submodules. if "submodules" in module and module["submodules"]: diff --git a/src/mots/module.py b/src/mots/module.py index d9094d7..a773f30 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -87,8 +87,8 @@ def __init__( if not self.peers and self.parent: self.peers = self.parent.peers - self.owner_names = [owner["nick"] for owner in self.owners] - self.peer_names = [peer["nick"] for peer in self.peers] + self.owner_names = [owner.get("nick", "") for owner in self.owners] + self.peer_names = [peer.get("nick", "") for peer in self.peers] def calculate_paths(self): """Calculate paths based on inclusions and exclusions. From 9c72723c1c51f21458163b484b7eddfb562a5fe1 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Wed, 2 Feb 2022 16:14:04 -0500 Subject: [PATCH 17/26] Minor change --- src/mots/directory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mots/directory.py b/src/mots/directory.py index 2697dd4..3d79a8f 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -164,7 +164,7 @@ def __radd__(self, query_result): class Person: """A class representing a person.""" - bmo_id: int = None + bmo_id: int name: str = "" info: str = "" nick: str = "" From 97cee6631c8299a851d2b5e8503e07a8e1a8b0b8 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Fri, 4 Feb 2022 12:49:53 -0500 Subject: [PATCH 18/26] code review feedback + other fixes --- Makefile | 13 +++-- mots.log | 115 ++++++++++++++++++++++++++++++++++++++++++ mots.yaml | 16 +++--- setup.cfg | 2 +- src/mots/cli.py | 6 +-- src/mots/config.py | 46 ++++++++++------- src/mots/directory.py | 24 +++++---- 7 files changed, 173 insertions(+), 49 deletions(-) create mode 100644 mots.log diff --git a/Makefile b/Makefile index d475b33..d7e20d7 100644 --- a/Makefile +++ b/Makefile @@ -77,13 +77,16 @@ else -@(python3 -m venv .mots-env && ([ $$? -eq 0 ] && echo "Found $(shell python3 --version)")) 2>/dev/null endif +.ONESHELL: .PHONY: dev dev: - # TODO: only run this if we can detect .mots-env virtual environment. - $(PYTHON) -m pip install --upgrade pip - $(PYTHON) -m pip install pip-tools - $(PYTHON) -m pip install -r requirements.txt - $(PYTHON) -m pip install -e . + deactivate + set -e + source ./.mots-env/bin/activate + python -m pip install --upgrade pip + python -m pip install pip-tools + python -m pip install -r requirements.txt + python -m pip install -e . .PHONY: serve-docs serve-docs: diff --git a/mots.log b/mots.log new file mode 100644 index 0000000..95172d1 --- /dev/null +++ b/mots.log @@ -0,0 +1,115 @@ +2022-02-02 16:13:06,408 logging DEBUG Logging configured with log file @ mots.log. +2022-02-02 16:13:06,408 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-02 16:13:06,419 directory DEBUG Updating index for main... +2022-02-02 16:13:06,419 module DEBUG Expanding **/* in main... +2022-02-02 16:13:06,483 module DEBUG Pattern **/* expanded to 10494 included path(s). +2022-02-02 16:13:12,653 logging DEBUG Logging configured with log file @ mots.log. +2022-02-02 16:13:12,654 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-02 16:13:12,664 directory DEBUG Updating index for main... +2022-02-02 16:13:12,664 module DEBUG Expanding **/* in main... +2022-02-02 16:13:12,725 module DEBUG Pattern **/* expanded to 10494 included path(s). +2022-02-02 16:13:12,734 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-02 16:13:13,274 directory DEBUG Initializing people directory with 1 people... +2022-02-02 16:13:13,274 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... +2022-02-02 16:13:13,275 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. +2022-02-02 16:13:13,288 cli DEBUG took 0.634101 seconds. +2022-02-04 09:10:13,207 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 09:10:13,207 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-04 09:10:47,421 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 09:10:47,421 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-04 09:10:47,432 directory DEBUG Updating index for main... +2022-02-04 09:10:47,432 module DEBUG Expanding **/* in main... +2022-02-04 09:10:47,978 module DEBUG Pattern **/* expanded to 10497 included path(s). +2022-02-04 09:10:52,537 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 09:10:52,537 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-04 09:10:52,548 directory DEBUG Updating index for main... +2022-02-04 09:10:52,548 module DEBUG Expanding **/* in main... +2022-02-04 09:10:52,615 module DEBUG Pattern **/* expanded to 10497 included path(s). +2022-02-04 09:10:52,625 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 09:10:53,569 bmo ERROR Error searching []: 400 +2022-02-04 09:10:53,570 directory DEBUG Initializing people directory with 0 people... +2022-02-04 09:10:53,580 cli DEBUG took 1.04311 seconds. +2022-02-04 10:00:19,423 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 10:00:19,424 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-04 10:01:01,048 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 10:01:01,048 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-04 10:01:01,062 directory DEBUG Updating index for main... +2022-02-04 10:01:01,063 module DEBUG Expanding **/* in main... +2022-02-04 10:01:01,138 module DEBUG Pattern **/* expanded to 9572 included path(s). +2022-02-04 10:01:01,148 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 10:01:01,826 bmo ERROR Error searching []: 400 +2022-02-04 10:01:01,827 directory DEBUG Initializing people directory with 0 people... +2022-02-04 10:20:09,259 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 10:20:09,260 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-04 10:20:09,271 directory DEBUG Updating index for main... +2022-02-04 10:20:09,271 module DEBUG Expanding **/* in main... +2022-02-04 10:20:09,331 module DEBUG Pattern **/* expanded to 9571 included path(s). +2022-02-04 10:20:09,340 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 10:20:10,138 bmo ERROR Error searching []: 400 +2022-02-04 10:20:10,139 directory DEBUG Initializing people directory with 0 people... +2022-02-04 10:20:10,151 cli DEBUG took 0.891843 seconds. +2022-02-04 10:20:32,011 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 10:20:32,011 cli DEBUG Calling with Namespace(debug=True, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-04 10:20:32,023 directory DEBUG Updating index for main... +2022-02-04 10:20:32,023 module DEBUG Expanding **/* in main... +2022-02-04 10:20:32,088 module DEBUG Pattern **/* expanded to 9571 included path(s). +2022-02-04 10:20:32,098 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 10:20:32,484 directory DEBUG Initializing people directory with 1 people... +2022-02-04 10:20:32,484 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... +2022-02-04 10:20:32,484 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. +2022-02-04 10:20:32,497 cli DEBUG took 0.485639 seconds. +2022-02-04 10:21:13,693 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 10:21:13,693 cli DEBUG Calling with Namespace(debug=True, func=, path=PosixPath('mots.yaml'), repo_path='.')... +2022-02-04 10:21:13,704 directory DEBUG Updating index for main... +2022-02-04 10:21:13,704 module DEBUG Expanding **/* in main... +2022-02-04 10:21:13,775 module DEBUG Pattern **/* expanded to 9571 included path(s). +2022-02-04 10:21:13,785 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 10:21:14,544 bmo ERROR Error searching []: 400 +2022-02-04 10:21:14,545 directory DEBUG Initializing people directory with 0 people... +2022-02-04 10:21:14,559 cli DEBUG took 0.865727 seconds. +2022-02-04 12:45:07,582 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 12:45:07,582 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), paths=['mots.yaml'])... +2022-02-04 12:45:07,590 directory DEBUG Updating index for main... +2022-02-04 12:45:07,590 module DEBUG Expanding **/* in main... +2022-02-04 12:45:07,649 module DEBUG Pattern **/* expanded to 9571 included path(s). +2022-02-04 12:45:07,656 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 12:45:08,089 directory DEBUG Initializing people directory with 1 people... +2022-02-04 12:45:08,089 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... +2022-02-04 12:45:08,089 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. +2022-02-04 12:45:08,089 directory DEBUG Query ('mots.yaml',) resolved to {'mots.yaml': []}. +2022-02-04 12:45:08,090 directory WARNING No bugzilla data was provided for user 633708 +2022-02-04 12:45:08,093 cli DEBUG took 0.510722 seconds. +2022-02-04 12:45:14,804 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 12:45:14,804 cli DEBUG Calling with Namespace(debug=True, func=, path=PosixPath('mots.yaml'), paths=['mots.yaml'])... +2022-02-04 12:45:14,812 directory DEBUG Updating index for main... +2022-02-04 12:45:14,812 module DEBUG Expanding **/* in main... +2022-02-04 12:45:14,869 module DEBUG Pattern **/* expanded to 9571 included path(s). +2022-02-04 12:45:14,877 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 12:45:15,266 directory DEBUG Initializing people directory with 1 people... +2022-02-04 12:45:15,266 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... +2022-02-04 12:45:15,267 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. +2022-02-04 12:45:15,267 directory DEBUG Query ('mots.yaml',) resolved to {'mots.yaml': []}. +2022-02-04 12:45:15,267 directory WARNING No bugzilla data was provided for user 633708 +2022-02-04 12:45:15,270 cli DEBUG took 0.465245 seconds. +2022-02-04 12:45:58,954 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 12:45:58,954 cli DEBUG Calling with Namespace(debug=True, func=, path=PosixPath('mots.yaml'), paths=['mots.yaml'])... +2022-02-04 12:45:58,962 directory DEBUG Updating index for main... +2022-02-04 12:45:58,962 module DEBUG Expanding **/* in main... +2022-02-04 12:45:59,030 module DEBUG Pattern **/* expanded to 9572 included path(s). +2022-02-04 12:45:59,038 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 12:45:59,427 directory DEBUG Initializing people directory with 1 people... +2022-02-04 12:45:59,428 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... +2022-02-04 12:45:59,429 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. +2022-02-04 12:45:59,429 directory DEBUG Query ('mots.yaml',) resolved to {'mots.yaml': []}. +2022-02-04 12:45:59,433 cli DEBUG took 0.478432 seconds. +2022-02-04 12:46:01,299 logging DEBUG Logging configured with log file @ mots.log. +2022-02-04 12:46:01,299 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), paths=['mots.yaml'])... +2022-02-04 12:46:01,307 directory DEBUG Updating index for main... +2022-02-04 12:46:01,307 module DEBUG Expanding **/* in main... +2022-02-04 12:46:01,367 module DEBUG Pattern **/* expanded to 9572 included path(s). +2022-02-04 12:46:01,375 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user +2022-02-04 12:46:01,770 directory DEBUG Initializing people directory with 1 people... +2022-02-04 12:46:01,770 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... +2022-02-04 12:46:01,771 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. +2022-02-04 12:46:01,771 directory DEBUG Query ('mots.yaml',) resolved to {'mots.yaml': []}. +2022-02-04 12:46:01,775 cli DEBUG took 0.475524 seconds. diff --git a/mots.yaml b/mots.yaml index 097ff00..0c7b33b 100644 --- a/mots.yaml +++ b/mots.yaml @@ -1,6 +1,12 @@ repo: mots created_at: '2022-02-02T14:48:50.574414' -updated_at: '2022-02-02T14:57:03.916934' +updated_at: '2022-02-04T10:21:14.554274' +people: + - &zeid + bmo_id: 633708 + name: Zeid Zabaneh + info: '[:zeid]' + nick: zeid modules: - machine_name: main name: Main @@ -8,12 +14,6 @@ modules: - '**/*' excludes: [] owners: - - &zeid - bmo_id: 633708 - name: Zeid Zabaneh - info: '[:zeid]' - nick: zeid + - *zeid peers: [] meta: -people: - - *zeid diff --git a/setup.cfg b/setup.cfg index db48d96..80df890 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ long_description_content_type = text/markdown url = https://github.com/mozilla-conduit/mots classifiers = License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) - Development Status :: 2 - Pre-Alpha + Development Status :: 3 - Alpha Intended Audience :: Developers Operating System :: OS Independent Programming Language :: Python :: 3 diff --git a/src/mots/cli.py b/src/mots/cli.py index 2ec7d9c..4ab3ab4 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -77,8 +77,7 @@ def query(args: argparse.Namespace) -> None: directory = Directory(file_config) directory.load() result = directory.query(*args.paths) - for path in result.path_map: - modules = result.path_map[path] + for path, modules in result.path_map.items(): module_names = ",".join([m.machine_name for m in modules]) print(f"{path}:{module_names}\n") @@ -130,9 +129,6 @@ def main(): logger.debug(f"{args.func} took {(et - st).total_seconds()} seconds.") else: parser.print_help() - for name, subparser in subparsers.choices.items(): - print() - subparser.print_help() def create_parser(): diff --git a/src/mots/config.py b/src/mots/config.py index 6d5b206..b12641a 100644 --- a/src/mots/config.py +++ b/src/mots/config.py @@ -49,8 +49,8 @@ def init(self): "repo": str(Path(self.path).resolve().parts[-2]), "created_at": now, "updated_at": None, - "modules": [], "people": [], + "modules": [], } self.write() logger.info(f"mots configuration initialized in {self.path}.") @@ -78,6 +78,7 @@ def clean(file_config: FileConfig, write: bool = True): :param file_config: an instance of :class:`FileConfig` :param write: if set to `True`, writes changes to disk. """ + people_keys = ("owners", "peers") file_config.load() directory = Directory(file_config) directory.load() @@ -85,26 +86,33 @@ def clean(file_config: FileConfig, write: bool = True): if "machine_name" not in module: module["machine_name"] = generate_machine_readable_name(module["name"]) - people_keys = ("owners", "peers") for key in people_keys: - if key in module and module[key]: - for i, person in enumerate(module[key]): - try: - module[key][i] = file_config.config["people"][ - directory.people.by_bmo_id[person["bmo_id"]] - ] - except KeyError: - file_config.config["people"].append(person) - module[key][i] = person + if key not in module or not module[key]: + continue + for i, person in enumerate(module[key]): + if person["bmo_id"] not in directory.people.by_bmo_id: + file_config.config["people"].append(person) + module[key][i] = person + directory.people.refresh_by_bmo_id() + else: + module[key][i] = file_config.config["people"][ + directory.people.by_bmo_id[person["bmo_id"]] + ] # Do the same for submodules. + # TODO: this should be refactored. if "submodules" in module and module["submodules"]: module["submodules"].sort(key=lambda x: x["name"]) for submodule in module["submodules"]: for key in people_keys: - if key in submodule and submodule[key]: - for i, person in enumerate(submodule[key]): - person = submodule[key][i] + if key not in submodule or not submodule[key]: + continue + for i, person in enumerate(submodule[key]): + if person["bmo_id"] not in directory.people.by_bmo_id: + file_config.config["people"].append(person) + submodule[key][i] = person + directory.people.refresh_by_bmo_id() + else: submodule[key][i] = file_config.config["people"][ directory.people.by_bmo_id[person["bmo_id"]] ] @@ -153,11 +161,11 @@ def validate(config: dict, repo_path: str): # Count machine name repetitions in a defaultdict. machine_names = defaultdict(int) - for m in modules: - machine_names[m["machine_name"]] += 1 - if "submodules" in m and m["submodules"]: - for sm in m["submodules"]: - machine_names[sm["machine_name"]] += 1 + for module in modules: + machine_names[module["machine_name"]] += 1 + if "submodules" in module and module["submodules"]: + for submodule in module["submodules"]: + machine_names[submodule["machine_name"]] += 1 machine_names = {name: count for name, count in machine_names.items() if count > 1} if machine_names: diff --git a/src/mots/directory.py b/src/mots/directory.py index 3d79a8f..f240375 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -127,23 +127,21 @@ class QueryResult: } def __init__(self, result, rejected): - data = {k: list() for k in self.data_keys} + data = {k: set() for k in self.data_keys} self.path_map = result for path in self.path_map: - data["paths"].append(path) - data["modules"] += self.path_map[path] + data["paths"].add(path) + data["modules"].update(self.path_map[path]) for module in data["modules"]: - # TODO: this conversion should happen elsewhere. - data["owners"] += [Person(**o) for o in module.owners] - data["peers"] += [Person(**p) for p in module.peers] + data["owners"].update([Person(**o) for o in module.owners]) + data["peers"].update([Person(**p) for p in module.peers]) - data["rejected_paths"] = rejected + data["rejected_paths"].update(rejected) - # Remove duplicate entries in all data attributes. for key in data: - setattr(self, key, list(set(data[key]))) + setattr(self, key, list(data[key])) def __add__(self, query_result): """Merge the data from both QueryResult objects.""" @@ -180,8 +178,6 @@ def __post_init__(self, bmo_data): parsed_real_name = parse_real_name(real_name) self.name = parsed_real_name["name"] self.info = parsed_real_name["info"] - else: - logger.warning(f"No bugzilla data was provided for user {self.bmo_id}") def __hash__(self): """Return a unique identifier for this person.""" @@ -195,6 +191,12 @@ class People: people: list = None serialized: list = None + def refresh_by_bmo_id(self): + """Refresh index positions of people by their bugzilla ID.""" + self.by_bmo_id = {} + for i, person in enumerate(self.people): + self.by_bmo_id[person["bmo_id"]] = i + def __init__(self, people, bmo_data: dict): logger.debug(f"Initializing people directory with {len(people)} people...") people = list(people) From a918a3775072c7689b7edbf1f4a2af6e4509a9b9 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Fri, 4 Feb 2022 12:57:59 -0500 Subject: [PATCH 19/26] pretty print + remove accidentally added file --- mots.log | 115 --------------------------------------------- src/mots/module.py | 3 ++ 2 files changed, 3 insertions(+), 115 deletions(-) delete mode 100644 mots.log diff --git a/mots.log b/mots.log deleted file mode 100644 index 95172d1..0000000 --- a/mots.log +++ /dev/null @@ -1,115 +0,0 @@ -2022-02-02 16:13:06,408 logging DEBUG Logging configured with log file @ mots.log. -2022-02-02 16:13:06,408 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-02 16:13:06,419 directory DEBUG Updating index for main... -2022-02-02 16:13:06,419 module DEBUG Expanding **/* in main... -2022-02-02 16:13:06,483 module DEBUG Pattern **/* expanded to 10494 included path(s). -2022-02-02 16:13:12,653 logging DEBUG Logging configured with log file @ mots.log. -2022-02-02 16:13:12,654 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-02 16:13:12,664 directory DEBUG Updating index for main... -2022-02-02 16:13:12,664 module DEBUG Expanding **/* in main... -2022-02-02 16:13:12,725 module DEBUG Pattern **/* expanded to 10494 included path(s). -2022-02-02 16:13:12,734 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-02 16:13:13,274 directory DEBUG Initializing people directory with 1 people... -2022-02-02 16:13:13,274 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... -2022-02-02 16:13:13,275 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. -2022-02-02 16:13:13,288 cli DEBUG took 0.634101 seconds. -2022-02-04 09:10:13,207 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 09:10:13,207 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-04 09:10:47,421 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 09:10:47,421 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-04 09:10:47,432 directory DEBUG Updating index for main... -2022-02-04 09:10:47,432 module DEBUG Expanding **/* in main... -2022-02-04 09:10:47,978 module DEBUG Pattern **/* expanded to 10497 included path(s). -2022-02-04 09:10:52,537 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 09:10:52,537 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-04 09:10:52,548 directory DEBUG Updating index for main... -2022-02-04 09:10:52,548 module DEBUG Expanding **/* in main... -2022-02-04 09:10:52,615 module DEBUG Pattern **/* expanded to 10497 included path(s). -2022-02-04 09:10:52,625 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 09:10:53,569 bmo ERROR Error searching []: 400 -2022-02-04 09:10:53,570 directory DEBUG Initializing people directory with 0 people... -2022-02-04 09:10:53,580 cli DEBUG took 1.04311 seconds. -2022-02-04 10:00:19,423 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 10:00:19,424 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-04 10:01:01,048 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 10:01:01,048 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-04 10:01:01,062 directory DEBUG Updating index for main... -2022-02-04 10:01:01,063 module DEBUG Expanding **/* in main... -2022-02-04 10:01:01,138 module DEBUG Pattern **/* expanded to 9572 included path(s). -2022-02-04 10:01:01,148 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 10:01:01,826 bmo ERROR Error searching []: 400 -2022-02-04 10:01:01,827 directory DEBUG Initializing people directory with 0 people... -2022-02-04 10:20:09,259 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 10:20:09,260 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-04 10:20:09,271 directory DEBUG Updating index for main... -2022-02-04 10:20:09,271 module DEBUG Expanding **/* in main... -2022-02-04 10:20:09,331 module DEBUG Pattern **/* expanded to 9571 included path(s). -2022-02-04 10:20:09,340 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 10:20:10,138 bmo ERROR Error searching []: 400 -2022-02-04 10:20:10,139 directory DEBUG Initializing people directory with 0 people... -2022-02-04 10:20:10,151 cli DEBUG took 0.891843 seconds. -2022-02-04 10:20:32,011 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 10:20:32,011 cli DEBUG Calling with Namespace(debug=True, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-04 10:20:32,023 directory DEBUG Updating index for main... -2022-02-04 10:20:32,023 module DEBUG Expanding **/* in main... -2022-02-04 10:20:32,088 module DEBUG Pattern **/* expanded to 9571 included path(s). -2022-02-04 10:20:32,098 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 10:20:32,484 directory DEBUG Initializing people directory with 1 people... -2022-02-04 10:20:32,484 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... -2022-02-04 10:20:32,484 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. -2022-02-04 10:20:32,497 cli DEBUG took 0.485639 seconds. -2022-02-04 10:21:13,693 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 10:21:13,693 cli DEBUG Calling with Namespace(debug=True, func=, path=PosixPath('mots.yaml'), repo_path='.')... -2022-02-04 10:21:13,704 directory DEBUG Updating index for main... -2022-02-04 10:21:13,704 module DEBUG Expanding **/* in main... -2022-02-04 10:21:13,775 module DEBUG Pattern **/* expanded to 9571 included path(s). -2022-02-04 10:21:13,785 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 10:21:14,544 bmo ERROR Error searching []: 400 -2022-02-04 10:21:14,545 directory DEBUG Initializing people directory with 0 people... -2022-02-04 10:21:14,559 cli DEBUG took 0.865727 seconds. -2022-02-04 12:45:07,582 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 12:45:07,582 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), paths=['mots.yaml'])... -2022-02-04 12:45:07,590 directory DEBUG Updating index for main... -2022-02-04 12:45:07,590 module DEBUG Expanding **/* in main... -2022-02-04 12:45:07,649 module DEBUG Pattern **/* expanded to 9571 included path(s). -2022-02-04 12:45:07,656 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 12:45:08,089 directory DEBUG Initializing people directory with 1 people... -2022-02-04 12:45:08,089 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... -2022-02-04 12:45:08,089 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. -2022-02-04 12:45:08,089 directory DEBUG Query ('mots.yaml',) resolved to {'mots.yaml': []}. -2022-02-04 12:45:08,090 directory WARNING No bugzilla data was provided for user 633708 -2022-02-04 12:45:08,093 cli DEBUG took 0.510722 seconds. -2022-02-04 12:45:14,804 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 12:45:14,804 cli DEBUG Calling with Namespace(debug=True, func=, path=PosixPath('mots.yaml'), paths=['mots.yaml'])... -2022-02-04 12:45:14,812 directory DEBUG Updating index for main... -2022-02-04 12:45:14,812 module DEBUG Expanding **/* in main... -2022-02-04 12:45:14,869 module DEBUG Pattern **/* expanded to 9571 included path(s). -2022-02-04 12:45:14,877 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 12:45:15,266 directory DEBUG Initializing people directory with 1 people... -2022-02-04 12:45:15,266 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... -2022-02-04 12:45:15,267 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. -2022-02-04 12:45:15,267 directory DEBUG Query ('mots.yaml',) resolved to {'mots.yaml': []}. -2022-02-04 12:45:15,267 directory WARNING No bugzilla data was provided for user 633708 -2022-02-04 12:45:15,270 cli DEBUG took 0.465245 seconds. -2022-02-04 12:45:58,954 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 12:45:58,954 cli DEBUG Calling with Namespace(debug=True, func=, path=PosixPath('mots.yaml'), paths=['mots.yaml'])... -2022-02-04 12:45:58,962 directory DEBUG Updating index for main... -2022-02-04 12:45:58,962 module DEBUG Expanding **/* in main... -2022-02-04 12:45:59,030 module DEBUG Pattern **/* expanded to 9572 included path(s). -2022-02-04 12:45:59,038 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 12:45:59,427 directory DEBUG Initializing people directory with 1 people... -2022-02-04 12:45:59,428 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... -2022-02-04 12:45:59,429 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. -2022-02-04 12:45:59,429 directory DEBUG Query ('mots.yaml',) resolved to {'mots.yaml': []}. -2022-02-04 12:45:59,433 cli DEBUG took 0.478432 seconds. -2022-02-04 12:46:01,299 logging DEBUG Logging configured with log file @ mots.log. -2022-02-04 12:46:01,299 cli DEBUG Calling with Namespace(debug=False, func=, path=PosixPath('mots.yaml'), paths=['mots.yaml'])... -2022-02-04 12:46:01,307 directory DEBUG Updating index for main... -2022-02-04 12:46:01,307 module DEBUG Expanding **/* in main... -2022-02-04 12:46:01,367 module DEBUG Pattern **/* expanded to 9572 included path(s). -2022-02-04 12:46:01,375 bmo DEBUG GET https://bugzilla.mozilla.org/rest/user -2022-02-04 12:46:01,770 directory DEBUG Initializing people directory with 1 people... -2022-02-04 12:46:01,770 directory DEBUG Adding person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) to roster... -2022-02-04 12:46:01,771 directory DEBUG Person ordereddict([('bmo_id', 633708), ('name', 'Zeid Zabaneh'), ('info', '[:zeid]'), ('nick', 'zeid')]) added to position 0. -2022-02-04 12:46:01,771 directory DEBUG Query ('mots.yaml',) resolved to {'mots.yaml': []}. -2022-02-04 12:46:01,775 cli DEBUG took 0.475524 seconds. diff --git a/src/mots/module.py b/src/mots/module.py index a773f30..02e9199 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -7,9 +7,12 @@ from __future__ import annotations import logging from pathlib import Path +import pprint logger = logging.getLogger(__name__) +print = pprint.PrettyPrinter().pprint + class Module: """A top-level module or a submodule. From 38e349043513ab0aeee8cbc79374ff2dfa00c449 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Fri, 4 Feb 2022 13:51:44 -0500 Subject: [PATCH 20/26] Minor changes --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 80df890..44d13fe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ version = 0.0.1.a0 author = Zeid Zabaneh author_email = zeid@mozilla.com description = Module Ownership in Tree System -long_description = "Full documentation can be found at https://mots.readthedocs.io/en/develop/." +long_description = "Full documentation can be found at https://mots.readthedocs.io/en/latest/." long_description_content_type = text/markdown url = https://github.com/mozilla-conduit/mots classifiers = @@ -25,7 +25,7 @@ packages = find: python_requires = >=3.7 install_requires = ruamel.yaml==0.17.16 - importlib_resources; python_version < "3.9" + importlib-resources==5.4.0; python_version < "3.9" [options.packages.find] where = src From fa9258f53afaedf228da15affa0b0c60aa334285 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Fri, 4 Feb 2022 15:33:40 -0500 Subject: [PATCH 21/26] add more circleci environments; code review feedback --- .circleci/config.yml | 19 +++++++++++++++++-- src/mots/module.py | 4 +--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7c5cd68..486a382 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,12 +4,15 @@ workflows: version: 2 test: jobs: + - test_3_10 - test_3_9 + - test_3_8 + - test_3_7 jobs: - test_3_9: + test_3_7: &test_template docker: - - image: circleci/python:3.9 + - image: circleci/python:3.7 steps: - checkout - run: @@ -34,3 +37,15 @@ jobs: path: /tmp/test-reports - store_artifacts: path: /tmo/test-reports + test_3_8: + <<: *test_template + docker: + - image: circleci/python:3.8 + test_3_9: + <<: *test_template + docker: + - image: circleci/python:3.9 + test_3_10: + <<: *test_template + docker: + - image: circleci/python:3.10 diff --git a/src/mots/module.py b/src/mots/module.py index 02e9199..72682bc 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -7,12 +7,10 @@ from __future__ import annotations import logging from pathlib import Path -import pprint +from pprint import pprint as print logger = logging.getLogger(__name__) -print = pprint.PrettyPrinter().pprint - class Module: """A top-level module or a submodule. From 271dbbe0af128629d40a43d54f38d930b608d81f Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Mon, 7 Feb 2022 09:50:06 -0500 Subject: [PATCH 22/26] Additional improvements: - generate separate requirement files for each supported python - update requirements to latest versions where applicable - update make commands to auto-generate requirements and environments --- .circleci/config.yml | 2 +- Makefile | 10 +- docker-compose.yml | 23 + requirements.in | 11 - requirements/base.in | 10 + requirements/get_filename | 8 + requirements/python3.10.txt | 517 ++++++++++++++++ requirements/python3.7.txt | 557 ++++++++++++++++++ .../python3.8.txt | 445 ++++++-------- requirements/python3.9.txt | 521 ++++++++++++++++ 10 files changed, 1823 insertions(+), 281 deletions(-) create mode 100644 docker-compose.yml delete mode 100644 requirements.in create mode 100644 requirements/base.in create mode 100755 requirements/get_filename create mode 100644 requirements/python3.10.txt create mode 100644 requirements/python3.7.txt rename requirements.txt => requirements/python3.8.txt (51%) create mode 100644 requirements/python3.9.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 486a382..01b175e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,7 +21,7 @@ jobs: set -e sudo apt-get install python3-venv mkdir -p /tmp/test-reports - make dev-env PY=python3.9 + make dev-env source .mots-env/bin/activate make dev - run: diff --git a/Makefile b/Makefile index d7e20d7..8b8a650 100644 --- a/Makefile +++ b/Makefile @@ -61,8 +61,11 @@ format: .PHONY: requirements requirements: - $ rm requirements.txt - $(PYTHON) -m piptools compile --generate-hashes + rm requirements/*.txt + docker-compose run generate-python3.7-requirements + docker-compose run generate-python3.8-requirements + docker-compose run generate-python3.9-requirements + docker-compose run generate-python3.10-requirements .ONESHELL: .PHONY: dev-env @@ -85,7 +88,8 @@ dev: source ./.mots-env/bin/activate python -m pip install --upgrade pip python -m pip install pip-tools - python -m pip install -r requirements.txt + ls -lla requirements + python -m pip install -r requirements/$$(./requirements/get_filename) python -m pip install -e . .PHONY: serve-docs diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ece4e3d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '2' + +services: + generate-python3.7-requirements: + image: python:3.7 + command: sh -c "cd /mots && pip install pip-tools && python -m piptools compile requirements/base.in --generate-hashes -o requirements/python3.7.txt" + volumes: + - .:/mots + generate-python3.8-requirements: + image: python:3.8 + command: sh -c "cd /mots && pip install pip-tools && python -m piptools compile requirements/base.in --generate-hashes -o requirements/python3.8.txt" + volumes: + - .:/mots + generate-python3.9-requirements: + image: python:3.9 + command: sh -c "cd /mots && pip install pip-tools && python -m piptools compile requirements/base.in --generate-hashes -o requirements/python3.9.txt" + volumes: + - .:/mots + generate-python3.10-requirements: + image: python:3.10 + command: sh -c "cd /mots && pip install pip-tools && python -m piptools compile requirements/base.in --generate-hashes -o requirements/python3.10.txt" + volumes: + - .:/mots diff --git a/requirements.in b/requirements.in deleted file mode 100644 index 7529886..0000000 --- a/requirements.in +++ /dev/null @@ -1,11 +0,0 @@ -black==21.7b0 -build==0.6.0.post1 -flake8-docstrings==1.6.0 -flake8==3.9.2 -pytest-cov==2.12.1 -pytest==6.2.5 -pyyaml==5.4.1 -ruamel.yaml==0.17.16 -sphinx-rtd-theme==1.0.0 -sphinx==4.2.0 -twine==3.4.2 diff --git a/requirements/base.in b/requirements/base.in new file mode 100644 index 0000000..8ed5865 --- /dev/null +++ b/requirements/base.in @@ -0,0 +1,10 @@ +black==22.1.0 +build==0.7.0 +flake8-docstrings==1.6.0 +flake8==4.0.1 +pytest-cov==3.0.0 +pytest==7.0.0 +ruamel.yaml==0.17.20 +sphinx-rtd-theme==1.0.0 +sphinx==4.3.2 +twine==3.8.0 diff --git a/requirements/get_filename b/requirements/get_filename new file mode 100755 index 0000000..1b87afc --- /dev/null +++ b/requirements/get_filename @@ -0,0 +1,8 @@ +#! /usr/bin/env python + +import platform + + +if __name__ == "__main__": + version = platform.python_version_tuple() + print("python%s.%s.txt" % version[0:2]) diff --git a/requirements/python3.10.txt b/requirements/python3.10.txt new file mode 100644 index 0000000..c32a65a --- /dev/null +++ b/requirements/python3.10.txt @@ -0,0 +1,517 @@ +# +# This file is autogenerated by pip-compile with python 3.10 +# To update, run: +# +# pip-compile --generate-hashes --output-file=requirements/python3.10.txt requirements/base.in +# +alabaster==0.7.12 \ + --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ + --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 + # via sphinx +attrs==21.4.0 \ + --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ + --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd + # via pytest +babel==2.9.1 \ + --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ + --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 + # via sphinx +black==22.1.0 \ + --hash=sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2 \ + --hash=sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71 \ + --hash=sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6 \ + --hash=sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5 \ + --hash=sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912 \ + --hash=sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866 \ + --hash=sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d \ + --hash=sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0 \ + --hash=sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321 \ + --hash=sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8 \ + --hash=sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd \ + --hash=sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3 \ + --hash=sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba \ + --hash=sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0 \ + --hash=sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5 \ + --hash=sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a \ + --hash=sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28 \ + --hash=sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c \ + --hash=sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1 \ + --hash=sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab \ + --hash=sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f \ + --hash=sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61 \ + --hash=sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3 + # via -r requirements/base.in +bleach==4.1.0 \ + --hash=sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da \ + --hash=sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994 + # via readme-renderer +build==0.7.0 \ + --hash=sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f \ + --hash=sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8 + # via -r requirements/base.in +certifi==2021.10.8 \ + --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ + --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 + # via requests +cffi==1.15.0 \ + --hash=sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3 \ + --hash=sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2 \ + --hash=sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636 \ + --hash=sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20 \ + --hash=sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728 \ + --hash=sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27 \ + --hash=sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66 \ + --hash=sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443 \ + --hash=sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0 \ + --hash=sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7 \ + --hash=sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39 \ + --hash=sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605 \ + --hash=sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a \ + --hash=sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37 \ + --hash=sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029 \ + --hash=sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139 \ + --hash=sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc \ + --hash=sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df \ + --hash=sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14 \ + --hash=sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880 \ + --hash=sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2 \ + --hash=sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a \ + --hash=sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e \ + --hash=sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474 \ + --hash=sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024 \ + --hash=sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8 \ + --hash=sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0 \ + --hash=sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e \ + --hash=sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a \ + --hash=sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e \ + --hash=sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032 \ + --hash=sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6 \ + --hash=sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e \ + --hash=sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b \ + --hash=sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e \ + --hash=sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954 \ + --hash=sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962 \ + --hash=sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c \ + --hash=sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4 \ + --hash=sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55 \ + --hash=sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962 \ + --hash=sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023 \ + --hash=sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c \ + --hash=sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6 \ + --hash=sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8 \ + --hash=sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382 \ + --hash=sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7 \ + --hash=sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc \ + --hash=sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997 \ + --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796 + # via cryptography +charset-normalizer==2.0.11 \ + --hash=sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45 \ + --hash=sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c + # via requests +click==8.0.3 \ + --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ + --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b + # via black +colorama==0.4.4 \ + --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ + --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 + # via twine +coverage[toml]==6.3.1 \ + --hash=sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c \ + --hash=sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0 \ + --hash=sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554 \ + --hash=sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb \ + --hash=sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2 \ + --hash=sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b \ + --hash=sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8 \ + --hash=sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba \ + --hash=sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734 \ + --hash=sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2 \ + --hash=sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f \ + --hash=sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0 \ + --hash=sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1 \ + --hash=sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd \ + --hash=sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687 \ + --hash=sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1 \ + --hash=sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c \ + --hash=sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa \ + --hash=sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8 \ + --hash=sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38 \ + --hash=sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8 \ + --hash=sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167 \ + --hash=sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27 \ + --hash=sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145 \ + --hash=sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa \ + --hash=sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a \ + --hash=sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed \ + --hash=sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793 \ + --hash=sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4 \ + --hash=sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217 \ + --hash=sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e \ + --hash=sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6 \ + --hash=sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d \ + --hash=sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320 \ + --hash=sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f \ + --hash=sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce \ + --hash=sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975 \ + --hash=sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10 \ + --hash=sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525 \ + --hash=sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda \ + --hash=sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1 + # via pytest-cov +cryptography==36.0.1 \ + --hash=sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3 \ + --hash=sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31 \ + --hash=sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac \ + --hash=sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf \ + --hash=sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316 \ + --hash=sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca \ + --hash=sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638 \ + --hash=sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94 \ + --hash=sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12 \ + --hash=sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173 \ + --hash=sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b \ + --hash=sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a \ + --hash=sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f \ + --hash=sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2 \ + --hash=sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9 \ + --hash=sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46 \ + --hash=sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903 \ + --hash=sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3 \ + --hash=sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1 \ + --hash=sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee + # via secretstorage +docutils==0.17.1 \ + --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ + --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 + # via + # readme-renderer + # sphinx + # sphinx-rtd-theme +flake8==4.0.1 \ + --hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \ + --hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d + # via + # -r requirements/base.in + # flake8-docstrings +flake8-docstrings==1.6.0 \ + --hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \ + --hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b + # via -r requirements/base.in +idna==3.3 \ + --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ + --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d + # via requests +imagesize==1.3.0 \ + --hash=sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c \ + --hash=sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d + # via sphinx +importlib-metadata==4.10.1 \ + --hash=sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6 \ + --hash=sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e + # via + # keyring + # twine +iniconfig==1.1.1 \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 + # via pytest +jeepney==0.7.1 \ + --hash=sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac \ + --hash=sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f + # via + # keyring + # secretstorage +jinja2==3.0.3 \ + --hash=sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 \ + --hash=sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7 + # via sphinx +keyring==23.5.0 \ + --hash=sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9 \ + --hash=sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261 + # via twine +markupsafe==2.0.1 \ + --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ + --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ + --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ + --hash=sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194 \ + --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ + --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ + --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ + --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ + --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ + --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ + --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ + --hash=sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a \ + --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ + --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ + --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ + --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ + --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ + --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ + --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ + --hash=sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047 \ + --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ + --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ + --hash=sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b \ + --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ + --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ + --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ + --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a \ + --hash=sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1 \ + --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ + --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ + --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ + --hash=sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee \ + --hash=sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f \ + --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ + --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ + --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ + --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ + --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ + --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ + --hash=sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86 \ + --hash=sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6 \ + --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ + --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ + --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ + --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ + --hash=sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e \ + --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ + --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ + --hash=sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f \ + --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ + --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ + --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ + --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ + --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ + --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ + --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ + --hash=sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a \ + --hash=sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207 \ + --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ + --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ + --hash=sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd \ + --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ + --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ + --hash=sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9 \ + --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ + --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ + --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ + --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ + --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 + # via jinja2 +mccabe==0.6.1 \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f + # via flake8 +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 + # via black +packaging==21.3 \ + --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ + --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 + # via + # bleach + # build + # pytest + # sphinx +pathspec==0.9.0 \ + --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ + --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 + # via black +pep517==0.12.0 \ + --hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \ + --hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161 + # via build +pkginfo==1.8.2 \ + --hash=sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff \ + --hash=sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc + # via twine +platformdirs==2.4.1 \ + --hash=sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca \ + --hash=sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda + # via black +pluggy==1.0.0 \ + --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ + --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 + # via pytest +py==1.11.0 \ + --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ + --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + # via pytest +pycodestyle==2.8.0 \ + --hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \ + --hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f + # via flake8 +pycparser==2.21 \ + --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ + --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 + # via cffi +pydocstyle==6.1.1 \ + --hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \ + --hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4 + # via flake8-docstrings +pyflakes==2.4.0 \ + --hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \ + --hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e + # via flake8 +pygments==2.11.2 \ + --hash=sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65 \ + --hash=sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a + # via + # readme-renderer + # sphinx +pyparsing==3.0.7 \ + --hash=sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea \ + --hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484 + # via packaging +pytest==7.0.0 \ + --hash=sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9 \ + --hash=sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11 + # via + # -r requirements/base.in + # pytest-cov +pytest-cov==3.0.0 \ + --hash=sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6 \ + --hash=sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470 + # via -r requirements/base.in +pytz==2021.3 \ + --hash=sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c \ + --hash=sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326 + # via babel +readme-renderer==32.0 \ + --hash=sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d \ + --hash=sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85 + # via twine +requests==2.27.1 \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d + # via + # requests-toolbelt + # sphinx + # twine +requests-toolbelt==0.9.1 \ + --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \ + --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 + # via twine +rfc3986==2.0.0 \ + --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ + --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c + # via twine +ruamel-yaml==0.17.20 \ + --hash=sha256:4b8a33c1efb2b443a93fcaafcfa4d2e445f8e8c29c528d9f5cdafb7cc9e4004c \ + --hash=sha256:810eef9c46523a3f77479c66267a4708255ebe806a2d540078408c2227f011af + # via -r requirements/base.in +ruamel-yaml-clib==0.2.6 \ + --hash=sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd \ + --hash=sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee \ + --hash=sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0 \ + --hash=sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7 \ + --hash=sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277 \ + --hash=sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104 \ + --hash=sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd \ + --hash=sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0 \ + --hash=sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78 \ + --hash=sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de \ + --hash=sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99 \ + --hash=sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527 \ + --hash=sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84 \ + --hash=sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7 \ + --hash=sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468 \ + --hash=sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b \ + --hash=sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94 \ + --hash=sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233 \ + --hash=sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb \ + --hash=sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5 \ + --hash=sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe \ + --hash=sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751 \ + --hash=sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502 \ + --hash=sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed \ + --hash=sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c + # via ruamel.yaml +secretstorage==3.3.1 \ + --hash=sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f \ + --hash=sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195 + # via keyring +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via bleach +snowballstemmer==2.2.0 \ + --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ + --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a + # via + # pydocstyle + # sphinx +sphinx==4.3.2 \ + --hash=sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c \ + --hash=sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851 + # via + # -r requirements/base.in + # sphinx-rtd-theme +sphinx-rtd-theme==1.0.0 \ + --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \ + --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c + # via -r requirements/base.in +sphinxcontrib-applehelp==1.0.2 \ + --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ + --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 + # via sphinx +sphinxcontrib-devhelp==1.0.2 \ + --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ + --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 + # via sphinx +sphinxcontrib-htmlhelp==2.0.0 \ + --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \ + --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 + # via sphinx +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 + # via sphinx +sphinxcontrib-qthelp==1.0.3 \ + --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ + --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 \ + --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ + --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 + # via sphinx +tomli==2.0.0 \ + --hash=sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224 \ + --hash=sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1 + # via + # black + # build + # coverage + # pep517 + # pytest +tqdm==4.62.3 \ + --hash=sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c \ + --hash=sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d + # via twine +twine==3.8.0 \ + --hash=sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19 \ + --hash=sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8 + # via -r requirements/base.in +urllib3==1.26.8 \ + --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ + --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c + # via + # requests + # twine +webencodings==0.5.1 \ + --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ + --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 + # via bleach +zipp==3.7.0 \ + --hash=sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d \ + --hash=sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375 + # via importlib-metadata + +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. +# setuptools diff --git a/requirements/python3.7.txt b/requirements/python3.7.txt new file mode 100644 index 0000000..9c7d317 --- /dev/null +++ b/requirements/python3.7.txt @@ -0,0 +1,557 @@ +# +# This file is autogenerated by pip-compile with python 3.7 +# To update, run: +# +# pip-compile --generate-hashes --output-file=requirements/python3.7.txt requirements/base.in +# +alabaster==0.7.12 \ + --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ + --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 + # via sphinx +attrs==21.4.0 \ + --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ + --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd + # via pytest +babel==2.9.1 \ + --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ + --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 + # via sphinx +black==22.1.0 \ + --hash=sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2 \ + --hash=sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71 \ + --hash=sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6 \ + --hash=sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5 \ + --hash=sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912 \ + --hash=sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866 \ + --hash=sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d \ + --hash=sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0 \ + --hash=sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321 \ + --hash=sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8 \ + --hash=sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd \ + --hash=sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3 \ + --hash=sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba \ + --hash=sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0 \ + --hash=sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5 \ + --hash=sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a \ + --hash=sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28 \ + --hash=sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c \ + --hash=sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1 \ + --hash=sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab \ + --hash=sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f \ + --hash=sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61 \ + --hash=sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3 + # via -r requirements/base.in +bleach==4.1.0 \ + --hash=sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da \ + --hash=sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994 + # via readme-renderer +build==0.7.0 \ + --hash=sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f \ + --hash=sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8 + # via -r requirements/base.in +certifi==2021.10.8 \ + --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ + --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 + # via requests +cffi==1.15.0 \ + --hash=sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3 \ + --hash=sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2 \ + --hash=sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636 \ + --hash=sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20 \ + --hash=sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728 \ + --hash=sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27 \ + --hash=sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66 \ + --hash=sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443 \ + --hash=sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0 \ + --hash=sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7 \ + --hash=sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39 \ + --hash=sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605 \ + --hash=sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a \ + --hash=sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37 \ + --hash=sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029 \ + --hash=sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139 \ + --hash=sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc \ + --hash=sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df \ + --hash=sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14 \ + --hash=sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880 \ + --hash=sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2 \ + --hash=sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a \ + --hash=sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e \ + --hash=sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474 \ + --hash=sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024 \ + --hash=sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8 \ + --hash=sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0 \ + --hash=sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e \ + --hash=sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a \ + --hash=sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e \ + --hash=sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032 \ + --hash=sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6 \ + --hash=sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e \ + --hash=sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b \ + --hash=sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e \ + --hash=sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954 \ + --hash=sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962 \ + --hash=sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c \ + --hash=sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4 \ + --hash=sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55 \ + --hash=sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962 \ + --hash=sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023 \ + --hash=sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c \ + --hash=sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6 \ + --hash=sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8 \ + --hash=sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382 \ + --hash=sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7 \ + --hash=sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc \ + --hash=sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997 \ + --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796 + # via cryptography +charset-normalizer==2.0.11 \ + --hash=sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45 \ + --hash=sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c + # via requests +click==8.0.3 \ + --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ + --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b + # via black +colorama==0.4.4 \ + --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ + --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 + # via twine +coverage[toml]==6.3.1 \ + --hash=sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c \ + --hash=sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0 \ + --hash=sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554 \ + --hash=sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb \ + --hash=sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2 \ + --hash=sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b \ + --hash=sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8 \ + --hash=sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba \ + --hash=sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734 \ + --hash=sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2 \ + --hash=sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f \ + --hash=sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0 \ + --hash=sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1 \ + --hash=sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd \ + --hash=sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687 \ + --hash=sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1 \ + --hash=sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c \ + --hash=sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa \ + --hash=sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8 \ + --hash=sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38 \ + --hash=sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8 \ + --hash=sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167 \ + --hash=sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27 \ + --hash=sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145 \ + --hash=sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa \ + --hash=sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a \ + --hash=sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed \ + --hash=sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793 \ + --hash=sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4 \ + --hash=sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217 \ + --hash=sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e \ + --hash=sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6 \ + --hash=sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d \ + --hash=sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320 \ + --hash=sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f \ + --hash=sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce \ + --hash=sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975 \ + --hash=sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10 \ + --hash=sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525 \ + --hash=sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda \ + --hash=sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1 + # via pytest-cov +cryptography==36.0.1 \ + --hash=sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3 \ + --hash=sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31 \ + --hash=sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac \ + --hash=sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf \ + --hash=sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316 \ + --hash=sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca \ + --hash=sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638 \ + --hash=sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94 \ + --hash=sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12 \ + --hash=sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173 \ + --hash=sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b \ + --hash=sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a \ + --hash=sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f \ + --hash=sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2 \ + --hash=sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9 \ + --hash=sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46 \ + --hash=sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903 \ + --hash=sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3 \ + --hash=sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1 \ + --hash=sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee + # via secretstorage +docutils==0.17.1 \ + --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ + --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 + # via + # readme-renderer + # sphinx + # sphinx-rtd-theme +flake8==4.0.1 \ + --hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \ + --hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d + # via + # -r requirements/base.in + # flake8-docstrings +flake8-docstrings==1.6.0 \ + --hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \ + --hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b + # via -r requirements/base.in +idna==3.3 \ + --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ + --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d + # via requests +imagesize==1.3.0 \ + --hash=sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c \ + --hash=sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d + # via sphinx +importlib-metadata==4.2.0 \ + --hash=sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b \ + --hash=sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31 + # via + # build + # click + # flake8 + # keyring + # pep517 + # pluggy + # pytest + # twine +iniconfig==1.1.1 \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 + # via pytest +jeepney==0.7.1 \ + --hash=sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac \ + --hash=sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f + # via + # keyring + # secretstorage +jinja2==3.0.3 \ + --hash=sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 \ + --hash=sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7 + # via sphinx +keyring==23.5.0 \ + --hash=sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9 \ + --hash=sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261 + # via twine +markupsafe==2.0.1 \ + --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ + --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ + --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ + --hash=sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194 \ + --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ + --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ + --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ + --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ + --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ + --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ + --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ + --hash=sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a \ + --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ + --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ + --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ + --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ + --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ + --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ + --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ + --hash=sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047 \ + --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ + --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ + --hash=sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b \ + --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ + --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ + --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ + --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a \ + --hash=sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1 \ + --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ + --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ + --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ + --hash=sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee \ + --hash=sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f \ + --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ + --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ + --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ + --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ + --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ + --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ + --hash=sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86 \ + --hash=sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6 \ + --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ + --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ + --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ + --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ + --hash=sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e \ + --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ + --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ + --hash=sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f \ + --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ + --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ + --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ + --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ + --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ + --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ + --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ + --hash=sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a \ + --hash=sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207 \ + --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ + --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ + --hash=sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd \ + --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ + --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ + --hash=sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9 \ + --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ + --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ + --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ + --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ + --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 + # via jinja2 +mccabe==0.6.1 \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f + # via flake8 +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 + # via black +packaging==21.3 \ + --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ + --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 + # via + # bleach + # build + # pytest + # sphinx +pathspec==0.9.0 \ + --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ + --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 + # via black +pep517==0.12.0 \ + --hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \ + --hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161 + # via build +pkginfo==1.8.2 \ + --hash=sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff \ + --hash=sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc + # via twine +platformdirs==2.4.1 \ + --hash=sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca \ + --hash=sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda + # via black +pluggy==1.0.0 \ + --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ + --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 + # via pytest +py==1.11.0 \ + --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ + --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + # via pytest +pycodestyle==2.8.0 \ + --hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \ + --hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f + # via flake8 +pycparser==2.21 \ + --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ + --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 + # via cffi +pydocstyle==6.1.1 \ + --hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \ + --hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4 + # via flake8-docstrings +pyflakes==2.4.0 \ + --hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \ + --hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e + # via flake8 +pygments==2.11.2 \ + --hash=sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65 \ + --hash=sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a + # via + # readme-renderer + # sphinx +pyparsing==3.0.7 \ + --hash=sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea \ + --hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484 + # via packaging +pytest==7.0.0 \ + --hash=sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9 \ + --hash=sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11 + # via + # -r requirements/base.in + # pytest-cov +pytest-cov==3.0.0 \ + --hash=sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6 \ + --hash=sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470 + # via -r requirements/base.in +pytz==2021.3 \ + --hash=sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c \ + --hash=sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326 + # via babel +readme-renderer==32.0 \ + --hash=sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d \ + --hash=sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85 + # via twine +requests==2.27.1 \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d + # via + # requests-toolbelt + # sphinx + # twine +requests-toolbelt==0.9.1 \ + --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \ + --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 + # via twine +rfc3986==2.0.0 \ + --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ + --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c + # via twine +ruamel-yaml==0.17.20 \ + --hash=sha256:4b8a33c1efb2b443a93fcaafcfa4d2e445f8e8c29c528d9f5cdafb7cc9e4004c \ + --hash=sha256:810eef9c46523a3f77479c66267a4708255ebe806a2d540078408c2227f011af + # via -r requirements/base.in +ruamel-yaml-clib==0.2.6 \ + --hash=sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd \ + --hash=sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee \ + --hash=sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0 \ + --hash=sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7 \ + --hash=sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277 \ + --hash=sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104 \ + --hash=sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd \ + --hash=sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0 \ + --hash=sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78 \ + --hash=sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de \ + --hash=sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99 \ + --hash=sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527 \ + --hash=sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84 \ + --hash=sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7 \ + --hash=sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468 \ + --hash=sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b \ + --hash=sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94 \ + --hash=sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233 \ + --hash=sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb \ + --hash=sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5 \ + --hash=sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe \ + --hash=sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751 \ + --hash=sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502 \ + --hash=sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed \ + --hash=sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c + # via ruamel.yaml +secretstorage==3.3.1 \ + --hash=sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f \ + --hash=sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195 + # via keyring +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via bleach +snowballstemmer==2.2.0 \ + --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ + --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a + # via + # pydocstyle + # sphinx +sphinx==4.3.2 \ + --hash=sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c \ + --hash=sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851 + # via + # -r requirements/base.in + # sphinx-rtd-theme +sphinx-rtd-theme==1.0.0 \ + --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \ + --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c + # via -r requirements/base.in +sphinxcontrib-applehelp==1.0.2 \ + --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ + --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 + # via sphinx +sphinxcontrib-devhelp==1.0.2 \ + --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ + --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 + # via sphinx +sphinxcontrib-htmlhelp==2.0.0 \ + --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \ + --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 + # via sphinx +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 + # via sphinx +sphinxcontrib-qthelp==1.0.3 \ + --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ + --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 \ + --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ + --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 + # via sphinx +tomli==2.0.0 \ + --hash=sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224 \ + --hash=sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1 + # via + # black + # build + # coverage + # pep517 + # pytest +tqdm==4.62.3 \ + --hash=sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c \ + --hash=sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d + # via twine +twine==3.8.0 \ + --hash=sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19 \ + --hash=sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8 + # via -r requirements/base.in +typed-ast==1.5.2 \ + --hash=sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e \ + --hash=sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344 \ + --hash=sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266 \ + --hash=sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a \ + --hash=sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd \ + --hash=sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d \ + --hash=sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837 \ + --hash=sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098 \ + --hash=sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e \ + --hash=sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27 \ + --hash=sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b \ + --hash=sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596 \ + --hash=sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76 \ + --hash=sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30 \ + --hash=sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4 \ + --hash=sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78 \ + --hash=sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca \ + --hash=sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985 \ + --hash=sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb \ + --hash=sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88 \ + --hash=sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7 \ + --hash=sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5 \ + --hash=sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e \ + --hash=sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7 + # via black +typing-extensions==4.0.1 \ + --hash=sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e \ + --hash=sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b + # via + # black + # importlib-metadata +urllib3==1.26.8 \ + --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ + --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c + # via + # requests + # twine +webencodings==0.5.1 \ + --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ + --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 + # via bleach +zipp==3.7.0 \ + --hash=sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d \ + --hash=sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375 + # via + # importlib-metadata + # pep517 + +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. +# setuptools diff --git a/requirements.txt b/requirements/python3.8.txt similarity index 51% rename from requirements.txt rename to requirements/python3.8.txt index f2132d7..b4625ea 100644 --- a/requirements.txt +++ b/requirements/python3.8.txt @@ -2,36 +2,53 @@ # This file is autogenerated by pip-compile with python 3.8 # To update, run: # -# pip-compile --generate-hashes +# pip-compile --generate-hashes --output-file=requirements/python3.8.txt requirements/base.in # alabaster==0.7.12 \ --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 # via sphinx -appdirs==1.4.4 \ - --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ - --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 - # via black -attrs==21.2.0 \ - --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ - --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb +attrs==21.4.0 \ + --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ + --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd # via pytest babel==2.9.1 \ --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 # via sphinx -black==21.7b0 \ - --hash=sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116 \ - --hash=sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219 - # via -r requirements.in +black==22.1.0 \ + --hash=sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2 \ + --hash=sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71 \ + --hash=sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6 \ + --hash=sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5 \ + --hash=sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912 \ + --hash=sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866 \ + --hash=sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d \ + --hash=sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0 \ + --hash=sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321 \ + --hash=sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8 \ + --hash=sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd \ + --hash=sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3 \ + --hash=sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba \ + --hash=sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0 \ + --hash=sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5 \ + --hash=sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a \ + --hash=sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28 \ + --hash=sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c \ + --hash=sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1 \ + --hash=sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab \ + --hash=sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f \ + --hash=sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61 \ + --hash=sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3 + # via -r requirements/base.in bleach==4.1.0 \ --hash=sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da \ --hash=sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994 # via readme-renderer -build==0.6.0.post1 \ - --hash=sha256:32290592c8ccf70ce84107962f6129407abf52cedaa752af28c0c95d99dfa2e7 \ - --hash=sha256:d8d8417caff47888274d677f984de509554637dd1ea952d467b027849b06d83b - # via -r requirements.in +build==0.7.0 \ + --hash=sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f \ + --hash=sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8 + # via -r requirements/base.in certifi==2021.10.8 \ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 @@ -88,9 +105,9 @@ cffi==1.15.0 \ --hash=sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997 \ --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796 # via cryptography -charset-normalizer==2.0.9 \ - --hash=sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721 \ - --hash=sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c +charset-normalizer==2.0.11 \ + --hash=sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45 \ + --hash=sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c # via requests click==8.0.3 \ --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ @@ -100,77 +117,70 @@ colorama==0.4.4 \ --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 # via twine -coverage==6.2 \ - --hash=sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0 \ - --hash=sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd \ - --hash=sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884 \ - --hash=sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48 \ - --hash=sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76 \ - --hash=sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0 \ - --hash=sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64 \ - --hash=sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685 \ - --hash=sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47 \ - --hash=sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d \ - --hash=sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840 \ - --hash=sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f \ - --hash=sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971 \ - --hash=sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c \ - --hash=sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a \ - --hash=sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de \ - --hash=sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17 \ - --hash=sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4 \ - --hash=sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521 \ - --hash=sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57 \ - --hash=sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b \ - --hash=sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282 \ - --hash=sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644 \ - --hash=sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475 \ - --hash=sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d \ - --hash=sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da \ - --hash=sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953 \ - --hash=sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2 \ - --hash=sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e \ - --hash=sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c \ - --hash=sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc \ - --hash=sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64 \ - --hash=sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74 \ - --hash=sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617 \ - --hash=sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3 \ - --hash=sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d \ - --hash=sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa \ - --hash=sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739 \ - --hash=sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8 \ - --hash=sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8 \ - --hash=sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781 \ - --hash=sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58 \ - --hash=sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9 \ - --hash=sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c \ - --hash=sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd \ - --hash=sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e \ - --hash=sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49 +coverage[toml]==6.3.1 \ + --hash=sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c \ + --hash=sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0 \ + --hash=sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554 \ + --hash=sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb \ + --hash=sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2 \ + --hash=sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b \ + --hash=sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8 \ + --hash=sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba \ + --hash=sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734 \ + --hash=sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2 \ + --hash=sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f \ + --hash=sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0 \ + --hash=sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1 \ + --hash=sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd \ + --hash=sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687 \ + --hash=sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1 \ + --hash=sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c \ + --hash=sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa \ + --hash=sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8 \ + --hash=sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38 \ + --hash=sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8 \ + --hash=sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167 \ + --hash=sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27 \ + --hash=sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145 \ + --hash=sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa \ + --hash=sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a \ + --hash=sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed \ + --hash=sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793 \ + --hash=sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4 \ + --hash=sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217 \ + --hash=sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e \ + --hash=sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6 \ + --hash=sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d \ + --hash=sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320 \ + --hash=sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f \ + --hash=sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce \ + --hash=sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975 \ + --hash=sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10 \ + --hash=sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525 \ + --hash=sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda \ + --hash=sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1 # via pytest-cov -cryptography==36.0.0 \ - --hash=sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681 \ - --hash=sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed \ - --hash=sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4 \ - --hash=sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568 \ - --hash=sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e \ - --hash=sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f \ - --hash=sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f \ - --hash=sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712 \ - --hash=sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e \ - --hash=sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58 \ - --hash=sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44 \ - --hash=sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6 \ - --hash=sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d \ - --hash=sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636 \ - --hash=sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba \ - --hash=sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120 \ - --hash=sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3 \ - --hash=sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d \ - --hash=sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b \ - --hash=sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81 \ - --hash=sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8 +cryptography==36.0.1 \ + --hash=sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3 \ + --hash=sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31 \ + --hash=sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac \ + --hash=sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf \ + --hash=sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316 \ + --hash=sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca \ + --hash=sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638 \ + --hash=sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94 \ + --hash=sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12 \ + --hash=sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173 \ + --hash=sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b \ + --hash=sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a \ + --hash=sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f \ + --hash=sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2 \ + --hash=sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9 \ + --hash=sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46 \ + --hash=sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903 \ + --hash=sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3 \ + --hash=sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1 \ + --hash=sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee # via secretstorage docutils==0.17.1 \ --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ @@ -179,16 +189,16 @@ docutils==0.17.1 \ # readme-renderer # sphinx # sphinx-rtd-theme -flake8==3.9.2 \ - --hash=sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b \ - --hash=sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907 +flake8==4.0.1 \ + --hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \ + --hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d # via - # -r requirements.in + # -r requirements/base.in # flake8-docstrings flake8-docstrings==1.6.0 \ --hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \ --hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b - # via -r requirements.in + # via -r requirements/base.in idna==3.3 \ --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d @@ -197,9 +207,9 @@ imagesize==1.3.0 \ --hash=sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c \ --hash=sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d # via sphinx -importlib-metadata==4.8.2 \ - --hash=sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100 \ - --hash=sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb +importlib-metadata==4.10.1 \ + --hash=sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6 \ + --hash=sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e # via # keyring # twine @@ -217,9 +227,9 @@ jinja2==3.0.3 \ --hash=sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 \ --hash=sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7 # via sphinx -keyring==23.4.0 \ - --hash=sha256:3dc0f66062a4f8f6f2ce30d6a516e6e623e6c3c2e76864204ceaf64695408f07 \ - --hash=sha256:88f206024295e3c6fb16bb0a60fb4bb7ec1185629dc5a729f12aa7c236d01387 +keyring==23.5.0 \ + --hash=sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9 \ + --hash=sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261 # via twine markupsafe==2.0.1 \ --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ @@ -320,6 +330,10 @@ pkginfo==1.8.2 \ --hash=sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff \ --hash=sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc # via twine +platformdirs==2.4.1 \ + --hash=sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca \ + --hash=sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda + # via black pluggy==1.0.0 \ --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 @@ -328,9 +342,9 @@ py==1.11.0 \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 # via pytest -pycodestyle==2.7.0 \ - --hash=sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068 \ - --hash=sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef +pycodestyle==2.8.0 \ + --hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \ + --hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f # via flake8 pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ @@ -340,148 +354,41 @@ pydocstyle==6.1.1 \ --hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \ --hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4 # via flake8-docstrings -pyflakes==2.3.1 \ - --hash=sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3 \ - --hash=sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db +pyflakes==2.4.0 \ + --hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \ + --hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e # via flake8 -pygments==2.10.0 \ - --hash=sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380 \ - --hash=sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6 +pygments==2.11.2 \ + --hash=sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65 \ + --hash=sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a # via # readme-renderer # sphinx -pyparsing==3.0.6 \ - --hash=sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4 \ - --hash=sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81 +pyparsing==3.0.7 \ + --hash=sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea \ + --hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484 # via packaging -pytest==6.2.5 \ - --hash=sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89 \ - --hash=sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134 +pytest==7.0.0 \ + --hash=sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9 \ + --hash=sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11 # via - # -r requirements.in + # -r requirements/base.in # pytest-cov -pytest-cov==2.12.1 \ - --hash=sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a \ - --hash=sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7 - # via -r requirements.in +pytest-cov==3.0.0 \ + --hash=sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6 \ + --hash=sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470 + # via -r requirements/base.in pytz==2021.3 \ --hash=sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c \ --hash=sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326 # via babel -pyyaml==5.4.1 \ - --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ - --hash=sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696 \ - --hash=sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393 \ - --hash=sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77 \ - --hash=sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922 \ - --hash=sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5 \ - --hash=sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8 \ - --hash=sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10 \ - --hash=sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc \ - --hash=sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018 \ - --hash=sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e \ - --hash=sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253 \ - --hash=sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347 \ - --hash=sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183 \ - --hash=sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541 \ - --hash=sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb \ - --hash=sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185 \ - --hash=sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc \ - --hash=sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db \ - --hash=sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa \ - --hash=sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46 \ - --hash=sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122 \ - --hash=sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b \ - --hash=sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63 \ - --hash=sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df \ - --hash=sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc \ - --hash=sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247 \ - --hash=sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6 \ - --hash=sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0 - # via -r requirements.in -readme-renderer==30.0 \ - --hash=sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc \ - --hash=sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8 +readme-renderer==32.0 \ + --hash=sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d \ + --hash=sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85 # via twine -regex==2021.11.10 \ - --hash=sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05 \ - --hash=sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f \ - --hash=sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc \ - --hash=sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4 \ - --hash=sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737 \ - --hash=sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a \ - --hash=sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4 \ - --hash=sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8 \ - --hash=sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d \ - --hash=sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03 \ - --hash=sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f \ - --hash=sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264 \ - --hash=sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a \ - --hash=sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef \ - --hash=sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f \ - --hash=sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da \ - --hash=sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc \ - --hash=sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063 \ - --hash=sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50 \ - --hash=sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a \ - --hash=sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49 \ - --hash=sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d \ - --hash=sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d \ - --hash=sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733 \ - --hash=sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00 \ - --hash=sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b \ - --hash=sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a \ - --hash=sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36 \ - --hash=sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345 \ - --hash=sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0 \ - --hash=sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732 \ - --hash=sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286 \ - --hash=sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12 \ - --hash=sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646 \ - --hash=sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667 \ - --hash=sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244 \ - --hash=sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29 \ - --hash=sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec \ - --hash=sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf \ - --hash=sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4 \ - --hash=sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449 \ - --hash=sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0 \ - --hash=sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a \ - --hash=sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d \ - --hash=sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129 \ - --hash=sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb \ - --hash=sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e \ - --hash=sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b \ - --hash=sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83 \ - --hash=sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf \ - --hash=sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e \ - --hash=sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b \ - --hash=sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942 \ - --hash=sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a \ - --hash=sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e \ - --hash=sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94 \ - --hash=sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc \ - --hash=sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a \ - --hash=sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e \ - --hash=sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965 \ - --hash=sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0 \ - --hash=sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36 \ - --hash=sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296 \ - --hash=sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec \ - --hash=sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23 \ - --hash=sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7 \ - --hash=sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe \ - --hash=sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6 \ - --hash=sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8 \ - --hash=sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b \ - --hash=sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb \ - --hash=sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b \ - --hash=sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30 \ - --hash=sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e - # via black -requests==2.26.0 \ - --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \ - --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 +requests==2.27.1 \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d # via # requests-toolbelt # sphinx @@ -490,21 +397,25 @@ requests-toolbelt==0.9.1 \ --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \ --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 # via twine -rfc3986==1.5.0 \ - --hash=sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835 \ - --hash=sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97 +rfc3986==2.0.0 \ + --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ + --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -ruamel.yaml==0.17.16 \ - --hash=sha256:1a771fc92d3823682b7f0893ad56cb5a5c87c48e62b5399d6f42c8759a583b33 \ - --hash=sha256:ea21da1198c4b41b8e7a259301cc9710d3b972bf8ba52f06218478e6802dd1f1 - # via -r requirements.in -ruamel.yaml.clib==0.2.6 \ +ruamel-yaml==0.17.20 \ + --hash=sha256:4b8a33c1efb2b443a93fcaafcfa4d2e445f8e8c29c528d9f5cdafb7cc9e4004c \ + --hash=sha256:810eef9c46523a3f77479c66267a4708255ebe806a2d540078408c2227f011af + # via -r requirements/base.in +ruamel-yaml-clib==0.2.6 \ --hash=sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd \ + --hash=sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee \ --hash=sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0 \ + --hash=sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7 \ --hash=sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277 \ --hash=sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104 \ --hash=sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd \ + --hash=sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0 \ --hash=sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78 \ + --hash=sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de \ --hash=sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99 \ --hash=sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527 \ --hash=sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84 \ @@ -535,16 +446,16 @@ snowballstemmer==2.2.0 \ # via # pydocstyle # sphinx -sphinx==4.2.0 \ - --hash=sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6 \ - --hash=sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0 +sphinx==4.3.2 \ + --hash=sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c \ + --hash=sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851 # via - # -r requirements.in + # -r requirements/base.in # sphinx-rtd-theme sphinx-rtd-theme==1.0.0 \ --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \ --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c - # via -r requirements.in + # via -r requirements/base.in sphinxcontrib-applehelp==1.0.2 \ --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 @@ -569,38 +480,40 @@ sphinxcontrib-serializinghtml==1.1.5 \ --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 # via sphinx -toml==0.10.2 \ - --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ - --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via - # pytest - # pytest-cov -tomli==1.2.2 \ - --hash=sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee \ - --hash=sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade +tomli==2.0.0 \ + --hash=sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224 \ + --hash=sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1 # via # black # build + # coverage # pep517 + # pytest tqdm==4.62.3 \ --hash=sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c \ --hash=sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d # via twine -twine==3.4.2 \ - --hash=sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218 \ - --hash=sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936 - # via -r requirements.in -urllib3==1.26.7 \ - --hash=sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece \ - --hash=sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844 - # via requests +twine==3.8.0 \ + --hash=sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19 \ + --hash=sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8 + # via -r requirements/base.in +typing-extensions==4.0.1 \ + --hash=sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e \ + --hash=sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b + # via black +urllib3==1.26.8 \ + --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ + --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c + # via + # requests + # twine webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 # via bleach -zipp==3.6.0 \ - --hash=sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832 \ - --hash=sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc +zipp==3.7.0 \ + --hash=sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d \ + --hash=sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375 # via importlib-metadata # WARNING: The following packages were not pinned, but pip requires them to be diff --git a/requirements/python3.9.txt b/requirements/python3.9.txt new file mode 100644 index 0000000..76f4a92 --- /dev/null +++ b/requirements/python3.9.txt @@ -0,0 +1,521 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# pip-compile --generate-hashes --output-file=requirements/python3.9.txt requirements/base.in +# +alabaster==0.7.12 \ + --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ + --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 + # via sphinx +attrs==21.4.0 \ + --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \ + --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd + # via pytest +babel==2.9.1 \ + --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ + --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 + # via sphinx +black==22.1.0 \ + --hash=sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2 \ + --hash=sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71 \ + --hash=sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6 \ + --hash=sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5 \ + --hash=sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912 \ + --hash=sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866 \ + --hash=sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d \ + --hash=sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0 \ + --hash=sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321 \ + --hash=sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8 \ + --hash=sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd \ + --hash=sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3 \ + --hash=sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba \ + --hash=sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0 \ + --hash=sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5 \ + --hash=sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a \ + --hash=sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28 \ + --hash=sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c \ + --hash=sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1 \ + --hash=sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab \ + --hash=sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f \ + --hash=sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61 \ + --hash=sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3 + # via -r requirements/base.in +bleach==4.1.0 \ + --hash=sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da \ + --hash=sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994 + # via readme-renderer +build==0.7.0 \ + --hash=sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f \ + --hash=sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8 + # via -r requirements/base.in +certifi==2021.10.8 \ + --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ + --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 + # via requests +cffi==1.15.0 \ + --hash=sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3 \ + --hash=sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2 \ + --hash=sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636 \ + --hash=sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20 \ + --hash=sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728 \ + --hash=sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27 \ + --hash=sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66 \ + --hash=sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443 \ + --hash=sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0 \ + --hash=sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7 \ + --hash=sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39 \ + --hash=sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605 \ + --hash=sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a \ + --hash=sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37 \ + --hash=sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029 \ + --hash=sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139 \ + --hash=sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc \ + --hash=sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df \ + --hash=sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14 \ + --hash=sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880 \ + --hash=sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2 \ + --hash=sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a \ + --hash=sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e \ + --hash=sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474 \ + --hash=sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024 \ + --hash=sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8 \ + --hash=sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0 \ + --hash=sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e \ + --hash=sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a \ + --hash=sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e \ + --hash=sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032 \ + --hash=sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6 \ + --hash=sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e \ + --hash=sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b \ + --hash=sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e \ + --hash=sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954 \ + --hash=sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962 \ + --hash=sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c \ + --hash=sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4 \ + --hash=sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55 \ + --hash=sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962 \ + --hash=sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023 \ + --hash=sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c \ + --hash=sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6 \ + --hash=sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8 \ + --hash=sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382 \ + --hash=sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7 \ + --hash=sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc \ + --hash=sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997 \ + --hash=sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796 + # via cryptography +charset-normalizer==2.0.11 \ + --hash=sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45 \ + --hash=sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c + # via requests +click==8.0.3 \ + --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ + --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b + # via black +colorama==0.4.4 \ + --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b \ + --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 + # via twine +coverage[toml]==6.3.1 \ + --hash=sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c \ + --hash=sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0 \ + --hash=sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554 \ + --hash=sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb \ + --hash=sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2 \ + --hash=sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b \ + --hash=sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8 \ + --hash=sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba \ + --hash=sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734 \ + --hash=sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2 \ + --hash=sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f \ + --hash=sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0 \ + --hash=sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1 \ + --hash=sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd \ + --hash=sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687 \ + --hash=sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1 \ + --hash=sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c \ + --hash=sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa \ + --hash=sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8 \ + --hash=sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38 \ + --hash=sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8 \ + --hash=sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167 \ + --hash=sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27 \ + --hash=sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145 \ + --hash=sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa \ + --hash=sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a \ + --hash=sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed \ + --hash=sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793 \ + --hash=sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4 \ + --hash=sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217 \ + --hash=sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e \ + --hash=sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6 \ + --hash=sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d \ + --hash=sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320 \ + --hash=sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f \ + --hash=sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce \ + --hash=sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975 \ + --hash=sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10 \ + --hash=sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525 \ + --hash=sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda \ + --hash=sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1 + # via pytest-cov +cryptography==36.0.1 \ + --hash=sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3 \ + --hash=sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31 \ + --hash=sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac \ + --hash=sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf \ + --hash=sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316 \ + --hash=sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca \ + --hash=sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638 \ + --hash=sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94 \ + --hash=sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12 \ + --hash=sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173 \ + --hash=sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b \ + --hash=sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a \ + --hash=sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f \ + --hash=sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2 \ + --hash=sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9 \ + --hash=sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46 \ + --hash=sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903 \ + --hash=sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3 \ + --hash=sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1 \ + --hash=sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee + # via secretstorage +docutils==0.17.1 \ + --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ + --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 + # via + # readme-renderer + # sphinx + # sphinx-rtd-theme +flake8==4.0.1 \ + --hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \ + --hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d + # via + # -r requirements/base.in + # flake8-docstrings +flake8-docstrings==1.6.0 \ + --hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \ + --hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b + # via -r requirements/base.in +idna==3.3 \ + --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ + --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d + # via requests +imagesize==1.3.0 \ + --hash=sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c \ + --hash=sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d + # via sphinx +importlib-metadata==4.10.1 \ + --hash=sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6 \ + --hash=sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e + # via + # keyring + # twine +iniconfig==1.1.1 \ + --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ + --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 + # via pytest +jeepney==0.7.1 \ + --hash=sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac \ + --hash=sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f + # via + # keyring + # secretstorage +jinja2==3.0.3 \ + --hash=sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 \ + --hash=sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7 + # via sphinx +keyring==23.5.0 \ + --hash=sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9 \ + --hash=sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261 + # via twine +markupsafe==2.0.1 \ + --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ + --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ + --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ + --hash=sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194 \ + --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ + --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ + --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ + --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ + --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ + --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ + --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ + --hash=sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a \ + --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ + --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ + --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ + --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ + --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ + --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ + --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ + --hash=sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047 \ + --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ + --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ + --hash=sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b \ + --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ + --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ + --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ + --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a \ + --hash=sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1 \ + --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ + --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ + --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ + --hash=sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee \ + --hash=sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f \ + --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ + --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ + --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ + --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ + --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ + --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ + --hash=sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86 \ + --hash=sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6 \ + --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ + --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ + --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ + --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ + --hash=sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e \ + --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ + --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ + --hash=sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f \ + --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ + --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ + --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ + --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ + --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ + --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ + --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ + --hash=sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a \ + --hash=sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207 \ + --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ + --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ + --hash=sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd \ + --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ + --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ + --hash=sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9 \ + --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ + --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ + --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ + --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ + --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 + # via jinja2 +mccabe==0.6.1 \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f + # via flake8 +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 + # via black +packaging==21.3 \ + --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ + --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 + # via + # bleach + # build + # pytest + # sphinx +pathspec==0.9.0 \ + --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ + --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 + # via black +pep517==0.12.0 \ + --hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \ + --hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161 + # via build +pkginfo==1.8.2 \ + --hash=sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff \ + --hash=sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc + # via twine +platformdirs==2.4.1 \ + --hash=sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca \ + --hash=sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda + # via black +pluggy==1.0.0 \ + --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ + --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 + # via pytest +py==1.11.0 \ + --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ + --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + # via pytest +pycodestyle==2.8.0 \ + --hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \ + --hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f + # via flake8 +pycparser==2.21 \ + --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ + --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 + # via cffi +pydocstyle==6.1.1 \ + --hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \ + --hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4 + # via flake8-docstrings +pyflakes==2.4.0 \ + --hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \ + --hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e + # via flake8 +pygments==2.11.2 \ + --hash=sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65 \ + --hash=sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a + # via + # readme-renderer + # sphinx +pyparsing==3.0.7 \ + --hash=sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea \ + --hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484 + # via packaging +pytest==7.0.0 \ + --hash=sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9 \ + --hash=sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11 + # via + # -r requirements/base.in + # pytest-cov +pytest-cov==3.0.0 \ + --hash=sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6 \ + --hash=sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470 + # via -r requirements/base.in +pytz==2021.3 \ + --hash=sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c \ + --hash=sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326 + # via babel +readme-renderer==32.0 \ + --hash=sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d \ + --hash=sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85 + # via twine +requests==2.27.1 \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d + # via + # requests-toolbelt + # sphinx + # twine +requests-toolbelt==0.9.1 \ + --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \ + --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 + # via twine +rfc3986==2.0.0 \ + --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ + --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c + # via twine +ruamel-yaml==0.17.20 \ + --hash=sha256:4b8a33c1efb2b443a93fcaafcfa4d2e445f8e8c29c528d9f5cdafb7cc9e4004c \ + --hash=sha256:810eef9c46523a3f77479c66267a4708255ebe806a2d540078408c2227f011af + # via -r requirements/base.in +ruamel-yaml-clib==0.2.6 \ + --hash=sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd \ + --hash=sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee \ + --hash=sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0 \ + --hash=sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7 \ + --hash=sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277 \ + --hash=sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104 \ + --hash=sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd \ + --hash=sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0 \ + --hash=sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78 \ + --hash=sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de \ + --hash=sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99 \ + --hash=sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527 \ + --hash=sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84 \ + --hash=sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7 \ + --hash=sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468 \ + --hash=sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b \ + --hash=sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94 \ + --hash=sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233 \ + --hash=sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb \ + --hash=sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5 \ + --hash=sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe \ + --hash=sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751 \ + --hash=sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502 \ + --hash=sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed \ + --hash=sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c + # via ruamel.yaml +secretstorage==3.3.1 \ + --hash=sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f \ + --hash=sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195 + # via keyring +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via bleach +snowballstemmer==2.2.0 \ + --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ + --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a + # via + # pydocstyle + # sphinx +sphinx==4.3.2 \ + --hash=sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c \ + --hash=sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851 + # via + # -r requirements/base.in + # sphinx-rtd-theme +sphinx-rtd-theme==1.0.0 \ + --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \ + --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c + # via -r requirements/base.in +sphinxcontrib-applehelp==1.0.2 \ + --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ + --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 + # via sphinx +sphinxcontrib-devhelp==1.0.2 \ + --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ + --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 + # via sphinx +sphinxcontrib-htmlhelp==2.0.0 \ + --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \ + --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 + # via sphinx +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 + # via sphinx +sphinxcontrib-qthelp==1.0.3 \ + --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ + --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 \ + --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ + --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 + # via sphinx +tomli==2.0.0 \ + --hash=sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224 \ + --hash=sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1 + # via + # black + # build + # coverage + # pep517 + # pytest +tqdm==4.62.3 \ + --hash=sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c \ + --hash=sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d + # via twine +twine==3.8.0 \ + --hash=sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19 \ + --hash=sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8 + # via -r requirements/base.in +typing-extensions==4.0.1 \ + --hash=sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e \ + --hash=sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b + # via black +urllib3==1.26.8 \ + --hash=sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed \ + --hash=sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c + # via + # requests + # twine +webencodings==0.5.1 \ + --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ + --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 + # via bleach +zipp==3.7.0 \ + --hash=sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d \ + --hash=sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375 + # via importlib-metadata + +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. +# setuptools From 898fa789541e2a68163733585426992723ade6d1 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Mon, 7 Feb 2022 15:32:39 -0500 Subject: [PATCH 23/26] code review feedback + small refactoring --- setup.cfg | 2 +- src/mots/bmo.py | 20 ++++++++--- src/mots/cli.py | 4 +-- src/mots/config.py | 15 +++++--- src/mots/directory.py | 84 ++++++++++++++----------------------------- src/mots/module.py | 10 +++--- src/mots/pmo.py | 2 +- src/mots/utils.py | 18 ---------- tests/test_utils.py | 36 ------------------- 9 files changed, 60 insertions(+), 131 deletions(-) diff --git a/setup.cfg b/setup.cfg index 44d13fe..9d503c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ package_dir = packages = find: python_requires = >=3.7 install_requires = - ruamel.yaml==0.17.16 + ruamel.yaml==0.17.20 importlib-resources==5.4.0; python_version < "3.9" [options.packages.find] diff --git a/src/mots/bmo.py b/src/mots/bmo.py index dae1bde..6293e00 100644 --- a/src/mots/bmo.py +++ b/src/mots/bmo.py @@ -13,10 +13,20 @@ logger = logging.getLogger(__name__) DEFAULT_BASE_URL = "https://bugzilla.mozilla.org/rest" -USER_AGENT = "mots" # TODO: improve this and include version. +USER_AGENT = "mots" BUGZILLA_API_KEY_ENV_VAR = "BUGZILLA_API_KEY" +def get_bmo_data(people: list) -> dict: + """Fetch an updated dictionary from Bugzilla with user data. + + Dictionary keys are set to user IDs, and values are set to various data. + """ + bmo_client = BMOClient() + bmo_data = bmo_client.get_users_by_ids([p["bmo_id"] for p in people]) + return bmo_data + + class BMOClient: """A thin wrapper as a Bugzilla API client.""" @@ -28,14 +38,14 @@ def __init__(self, token: str = None, base_url: str = DEFAULT_BASE_URL): f"{BUGZILLA_API_KEY_ENV_VAR} environment variable missing," " and no other token was explicitly provided" ) - self.headers = {"X-BUGZILLA-API-KEY": token, "User-Agent": USER_AGENT} - self.base_url = base_url + self._headers = {"X-BUGZILLA-API-KEY": token, "User-Agent": USER_AGENT} + self._base_url = base_url def _get(self, path: str, params=None): - logger.debug(f"GET {self.base_url}/{path}") + logger.debug(f"GET {self._base_url}/{path}") params = params or {} response = requests.get( - f"{self.base_url}/{path}", headers=self.headers, params=params + f"{self._base_url}/{path}", headers=self._headers, params=params ) return response diff --git a/src/mots/cli.py b/src/mots/cli.py index 4ab3ab4..b89ac35 100644 --- a/src/mots/cli.py +++ b/src/mots/cli.py @@ -87,9 +87,9 @@ def add(args: argparse.Namespace) -> None: params = { "machine_name": input( "Enter a machine name (alphanumeric characters and underscores)" - " for the new module: " + " for the new module (e.g. core_accessibility): " ), - "name": input("Enter a human readable name: "), + "name": input("Enter a human readable name (e.g. Core: Accessibility): "), "owners": get_list_input("Enter a comma separated list of owner bugzilla IDs"), "peers": get_list_input("Enter a comma separated list of peer bugzilla IDs"), "includes": get_list_input("Enter a comma separated list of paths to include"), diff --git a/src/mots/config.py b/src/mots/config.py index b12641a..686a87e 100644 --- a/src/mots/config.py +++ b/src/mots/config.py @@ -11,7 +11,8 @@ from pathlib import Path from ruamel.yaml import YAML -from mots.directory import Directory +from mots.bmo import get_bmo_data +from mots.directory import Directory, People from mots.module import Module from mots.utils import generate_machine_readable_name @@ -46,7 +47,7 @@ def init(self): # File does not exist, create it. now = datetime.now().isoformat() self.config = { - "repo": str(Path(self.path).resolve().parts[-2]), + "repo": str(Path(self.path).resolve().parent.name), "created_at": now, "updated_at": None, "people": [], @@ -82,6 +83,14 @@ def clean(file_config: FileConfig, write: bool = True): file_config.load() directory = Directory(file_config) directory.load() + + people = list(file_config.config["people"]) + + bmo_data = get_bmo_data(people) + updated_people = People(people, bmo_data) + logger.debug("Updating people configuration based on BMO data...") + file_config.config["people"] = updated_people.serialized + for i, module in enumerate(file_config.config["modules"]): if "machine_name" not in module: module["machine_name"] = generate_machine_readable_name(module["name"]) @@ -100,7 +109,6 @@ def clean(file_config: FileConfig, write: bool = True): ] # Do the same for submodules. - # TODO: this should be refactored. if "submodules" in module and module["submodules"]: module["submodules"].sort(key=lambda x: x["name"]) for submodule in module["submodules"]: @@ -208,7 +216,6 @@ def add( break else: modules.append(serialized) - # TODO: do better validation here, via schema validation or otherwise if write: file_config.write() diff --git a/src/mots/directory.py b/src/mots/directory.py index f240375..d4e71cf 100644 --- a/src/mots/directory.py +++ b/src/mots/directory.py @@ -8,9 +8,7 @@ from collections import defaultdict from dataclasses import asdict from dataclasses import dataclass -from dataclasses import InitVar import logging -from mots.bmo import BMOClient from mots.module import Module from mots.utils import parse_real_name @@ -23,16 +21,6 @@ logger = logging.getLogger(__name__) -def _get_bmo_data(people: list) -> dict: - """Fetch an updated dictionary from Bugzilla with user data. - - Dictionary keys are set to user IDs, and values are set to various data. - """ - bmo_client = BMOClient() - bmo_data = bmo_client.get_users_by_ids([p["bmo_id"] for p in people]) - return bmo_data - - class Directory: """Mots directory and path index.""" @@ -87,11 +75,7 @@ def load(self, full_paths: bool = False, query_bmo=True): # Load people directory people = list(self.config_handle.config["people"]) - bmo_data = _get_bmo_data(people) if query_bmo else {} - self.people = People(people, bmo_data) - if self.people.serialized != list(self.config_handle.config["people"]): - logger.debug("People directory modified, updating configuration...") - self.config_handle.config["people"] = self.people.serialized + self.people = People(people, {}) def query(self, *paths: str) -> QueryResult: """Query given paths and return a list of corresponding modules.""" @@ -111,13 +95,6 @@ def query(self, *paths: str) -> QueryResult: class QueryResult: """Helper class to simplify query result interpretation.""" - paths: list = None - path_map: dict = None - rejected_paths: list = None - modules: list = None - owners: list = None - peers: list = None - data_keys = { "paths", "rejected_paths", @@ -163,54 +140,45 @@ class Person: """A class representing a person.""" bmo_id: int - name: str = "" - info: str = "" - nick: str = "" - bmo_data: InitVar[dict] = None - - def __post_init__(self, bmo_data): - """Refresh BMO data from BMO API.""" - if bmo_data: - self.nick = bmo_data.get("nick", "") - self.bmo_id = bmo_data.get("id", self.bmo_id) - real_name = bmo_data.get("real_name", "") - - parsed_real_name = parse_real_name(real_name) - self.name = parsed_real_name["name"] - self.info = parsed_real_name["info"] + name: str + info: str + nick: str def __hash__(self): """Return a unique identifier for this person.""" - return int(self.bmo_id) + return self.bmo_id class People: """A people directory searchable by name, email, or BMO ID.""" - by_bmo_id: dict = None - people: list = None - serialized: list = None - - def refresh_by_bmo_id(self): - """Refresh index positions of people by their bugzilla ID.""" - self.by_bmo_id = {} - for i, person in enumerate(self.people): - self.by_bmo_id[person["bmo_id"]] = i - def __init__(self, people, bmo_data: dict): logger.debug(f"Initializing people directory with {len(people)} people...") - people = list(people) + self.people = [] self.by_bmo_id = {} + + people = list(people) for i, person in enumerate(people): logger.debug(f"Adding person {person} to roster...") - # TODO: should have a fallback here without BMO data. - self.people.append( - Person( - bmo_id=person["bmo_id"], - bmo_data=bmo_data.get(person["bmo_id"]), - ) - ) + + bmo_id = person["bmo_id"] = int(person["bmo_id"]) + if bmo_id in bmo_data and bmo_data[bmo_id]: + # Update person's data base on BMO data. + bmo_datum = bmo_data[person["bmo_id"]] + person["nick"] = bmo_datum.get("nick", "") + + parsed_real_name = parse_real_name(bmo_datum["real_name"]) + person["name"] = parsed_real_name["name"] + person["info"] = parsed_real_name["info"] + + self.people.append(Person(**person)) self.by_bmo_id[person["bmo_id"]] = i logger.debug(f"Person {person} added to position {i}.") self.serialized = [asdict(person) for person in self.people] + + def refresh_by_bmo_id(self): + """Refresh index positions of people by their bugzilla ID.""" + self.by_bmo_id = {} + for i, person in enumerate(self.people): + self.by_bmo_id[person.bmo_id] = i diff --git a/src/mots/module.py b/src/mots/module.py index 72682bc..c8c2eb1 100644 --- a/src/mots/module.py +++ b/src/mots/module.py @@ -39,10 +39,10 @@ def __init__( self, machine_name: str, repo_path: str, - name: str = None, - description: str = None, - includes: str = None, - excludes: str = None, + name: str = "", + description: str = "", + includes: list = None, + excludes: list = None, owners: list[str] = None, peers: list[str] = None, meta: dict = None, @@ -163,8 +163,6 @@ def validate(self): if not self.calculate_paths(): errors.append(f"No valid paths were found in {self.machine_name}.") - # TODO: validate people - if self.submodules: for submodule in self.submodules: errors += submodule.validate() diff --git a/src/mots/pmo.py b/src/mots/pmo.py index f1d572d..2868abf 100644 --- a/src/mots/pmo.py +++ b/src/mots/pmo.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) DEFAULT_BASE_URL = "https://people.mozilla.org/api/v4/" -USER_AGENT = "mots" # TODO: improve this and include version. +USER_AGENT = "mots" PMO_COOKIE_ENV_VAR = "PMO_COOKIE" diff --git a/src/mots/utils.py b/src/mots/utils.py index 8211350..6167bf3 100644 --- a/src/mots/utils.py +++ b/src/mots/utils.py @@ -31,24 +31,6 @@ def get_list_input(text: str): return [e.strip() for e in user_input if e] -def parse_user_string(string): - """Return user data based on provided string. - - Example: - >>> test = parse_user_string("charlie jones (cj) ") - >>> print(test) - >>> {"meta": "cj", "email": "cjones@example.org", "name": "charlie jones"} - """ - logger.debug(f"Parsing provided string {string}...") - pattern = re.compile( - r"^((?P([\w\-'.]+)( +[\w\-'.]+)*)\s?)" - r"(\((?P.*)\))?\s?(\<(?P.*)\>)?$" - ) - match = pattern.match(string) - if match: - return match.groupdict() - - def parse_real_name(real_name): """Parse real_name into name and info.""" pattern = re.compile(r"^(?P[\w\ ]+?)?\ ?(?P[\(\[\|\:].*)?$") diff --git a/tests/test_utils.py b/tests/test_utils.py index 07fb345..f3849c0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,7 +6,6 @@ from mots.utils import ( generate_machine_readable_name, - parse_user_string, parse_real_name, ) @@ -19,41 +18,6 @@ def test_generate_machine_readable_name(): assert generate_machine_readable_name("test: testing") == "test_testing" -def test_parse_user_string(): - test = parse_user_string("Robin Smith (robin) ") - assert test == { - "name": "Robin Smith", - "meta": "robin", - "email": "robin@mozilla.com", - } - - test = parse_user_string("Robin Smith ") - assert test == {"name": "Robin Smith", "meta": None, "email": "robin@mozilla.com"} - - test = parse_user_string("Robin Smith") - assert test == {"name": "Robin Smith", "meta": None, "email": None} - - test = parse_user_string("robin ") - assert test == {"name": "robin", "meta": None, "email": "robin@mozilla.com"} - - test = parse_user_string("robin middle smith ") - assert test == { - "name": "robin middle smith", - "meta": None, - "email": "robin@mozilla.com", - } - - test = parse_user_string("r. middle double-o'smith (meta) ") - assert test == { - "name": "r. middle double-o'smith", - "meta": "meta", - "email": "robin@mozilla.com", - } - - test = parse_user_string("") - assert test is None - - def test_parse_real_name(): assert parse_real_name("tëstér testerson (:test) [te/st]") == { "name": "tëstér testerson", From 5ef6ce20314988596bdf3b657b1200c5ef7b4525 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Tue, 8 Feb 2022 08:30:27 -0500 Subject: [PATCH 24/26] remove extra line in Makefile --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 8b8a650..a7b5f07 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,6 @@ dev: source ./.mots-env/bin/activate python -m pip install --upgrade pip python -m pip install pip-tools - ls -lla requirements python -m pip install -r requirements/$$(./requirements/get_filename) python -m pip install -e . From 56d380fa3658f75f604133c71f6b7b1f574c23aa Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Tue, 8 Feb 2022 08:36:25 -0500 Subject: [PATCH 25/26] update copyright --- documentation/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/conf.py b/documentation/conf.py index 65b4d62..d4300a5 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -23,7 +23,7 @@ # -- Project information ----------------------------------------------------- project = "mots" -copyright = "2021 Mozilla Inc." +copyright = "2021 - 2022 Mozilla Inc." author = "Zeid Zabaneh " From fb36034ad631a55c1e379e31ce42bf859bd701c7 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Tue, 8 Feb 2022 08:37:33 -0500 Subject: [PATCH 26/26] develop -> latest in README docs reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca3c257..73e6853 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ mots - Module Ownership in Tree System ====================================== -[Full documentation](https://mots.readthedocs.io/en/develop/) can be viewed online. +[Full documentation](https://mots.readthedocs.io/en/latest/) can be viewed online. ### Contributing