├── tests ├── __init__.py ├── test_data │ └── invalid_unicode_chars.txt ├── test_stdin_parsing.py ├── test_correct_tag_names.py └── conftest.py ├── tw_hooks ├── py.typed ├── types.py ├── __init__.py ├── base_hooks │ ├── __init__.py │ ├── on_launch_hook.py │ ├── on_add_hook.py │ ├── on_exit_hook.py │ ├── on_modify_hook.py │ └── base_hook.py ├── hooks │ ├── __init__.py │ ├── correct_tag_names.py │ ├── auto_tag_based_on_tags.py │ ├── detect_mutually_exclusive_tags.py │ ├── warn_on_task_congestion.py │ └── post_latest_start_to_i3_status.py ├── utils.py └── scripts │ └── install_hook_shims.py ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yml ├── .markdown-link-check.json ├── misc ├── logo.png ├── logo.svg ├── hook-only.png └── logo_attribution.md ├── .coveragerc ├── CHANGELOG.md ├── LICENSE.md ├── .gitignore ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── pyproject.toml ├── CODE_OF_CONDUCT.md ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tw_hooks/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bergercookie] 2 | -------------------------------------------------------------------------------- /.markdown-link-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [] 3 | } 4 | -------------------------------------------------------------------------------- /misc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergercookie/tw-hooks/HEAD/misc/logo.png -------------------------------------------------------------------------------- /misc/logo.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergercookie/tw-hooks/HEAD/misc/logo.svg -------------------------------------------------------------------------------- /misc/hook-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergercookie/tw-hooks/HEAD/misc/hook-only.png -------------------------------------------------------------------------------- /misc/logo_attribution.md: -------------------------------------------------------------------------------- 1 | Parts for the logo were downloaded from . 2 | -------------------------------------------------------------------------------- /tests/test_data/invalid_unicode_chars.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bergercookie/tw-hooks/HEAD/tests/test_data/invalid_unicode_chars.txt -------------------------------------------------------------------------------- /tw_hooks/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Literal, Mapping 2 | 3 | TaskT = Dict[str, Any] 4 | MapOfTags = Mapping[str, str] 5 | ListOfTagsList = List[List[str]] 6 | Retcode = Literal[0, 1] 7 | -------------------------------------------------------------------------------- /tw_hooks/__init__.py: -------------------------------------------------------------------------------- 1 | """Base hooks.""" 2 | from .base_hooks import BaseHook, OnAddHook, OnExitHook, OnLaunchHook, OnModifyHook 3 | 4 | __all__ = ["BaseHook", "OnExitHook", "OnModifyHook", "OnAddHook", "OnLaunchHook"] 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = tw_hooks 4 | 5 | [report] 6 | exclude_lines = 7 | if self.debug: 8 | pragma: no cover 9 | raise NotImplementedError 10 | if __name__ == .__main__.: 11 | ignore_errors = True 12 | omit = 13 | test/* 14 | -------------------------------------------------------------------------------- /tw_hooks/base_hooks/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_hook import BaseHook 2 | from .on_add_hook import OnAddHook 3 | from .on_exit_hook import OnExitHook 4 | from .on_launch_hook import OnLaunchHook 5 | from .on_modify_hook import OnModifyHook 6 | 7 | __all__ = ["BaseHook", "OnExitHook", "OnModifyHook", "OnAddHook", "OnLaunchHook"] 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | 7 | ## 0.1.0 (2022-06-02) 8 | 9 | * Bootstrap project via 10 | [python_bootstrap](https://github.com/bergercookie/python_package_cookiecutter) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tw_hooks/base_hooks/on_launch_hook.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import final 3 | 4 | from tw_hooks.base_hooks.base_hook import BaseHook 5 | 6 | 7 | class OnLaunchHook(BaseHook): 8 | """On launch hook base class.""" 9 | 10 | @final 11 | def on_launch(self): 12 | """Entrypoint - to be called by the Hook shim.""" 13 | return self._on_launch() 14 | 15 | @abstractmethod 16 | def _on_launch(self): 17 | """Implement this in your hook.""" 18 | 19 | @classmethod 20 | def entrypoint(cls) -> str: 21 | return "on_launch" 22 | 23 | @classmethod 24 | def require_stdin(cls) -> bool: 25 | return False 26 | 27 | @classmethod 28 | def shim_prefix(cls) -> str: 29 | return "on-launch" 30 | -------------------------------------------------------------------------------- /tw_hooks/hooks/__init__.py: -------------------------------------------------------------------------------- 1 | """Module file for the concrete hook implementations.""" 2 | 3 | __all__ = ["import_concrete_hooks"] 4 | 5 | 6 | def import_concrete_hooks(): 7 | """Import all the concrete hook implementations.""" 8 | from .auto_tag_based_on_tags import AutoTagBasedOnTags 9 | from .correct_tag_names import CorrectTagNames 10 | from .detect_mutually_exclusive_tags import DetectMutuallyExclusiveTags 11 | from .post_latest_start_to_i3_status import PostLatestSTartToI3Status 12 | from .warn_on_task_congestion import WarnOnTaskCongestion 13 | 14 | __all__.extend( 15 | [ 16 | "AutoTagBasedOnTags", 17 | "CorrectTagNames", 18 | "DetectMutuallyExclusiveTags", 19 | "PostLatestSTartToI3Status", 20 | "WarnOnTaskCongestion", 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ### To Reproduce 14 | 15 | Steps to reproduce the behavior: 16 | 17 | ### Expected Behavior 18 | 19 | A clear and concise description of what you expected to happen. 20 | 21 | ### 📸 Screenshots 22 | 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | ### Versions 26 | 27 | #### OS Version (`cat /etc/*-release; uname -a`) 28 | 29 | TODO 30 | 31 | #### Python version (`python --version; python3 --version`) 32 | 33 | TODO 34 | 35 | TODO 36 | 37 | ### Additional context 38 | 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /tw_hooks/base_hooks/on_add_hook.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import List, final 3 | 4 | from tw_hooks.base_hooks.base_hook import BaseHook 5 | from tw_hooks.types import TaskT 6 | from tw_hooks.utils import stdin_lines_to_json 7 | 8 | 9 | class OnAddHook(BaseHook): 10 | """On add hook base class.""" 11 | 12 | @final 13 | def on_add(self, stdin_lines: List[str]): 14 | """Entrypoint - to be called by the Hook shim.""" 15 | return self._on_add(stdin_lines_to_json(stdin_lines)[0]) 16 | 17 | @abstractmethod 18 | def _on_add(self, added_task: TaskT): 19 | """Implement this in your hook.""" 20 | 21 | @classmethod 22 | def entrypoint(cls) -> str: 23 | return "on_add" 24 | 25 | @classmethod 26 | def require_stdin(cls) -> bool: 27 | return True 28 | 29 | @classmethod 30 | def shim_prefix(cls) -> str: 31 | return "on-add" 32 | -------------------------------------------------------------------------------- /tw_hooks/base_hooks/on_exit_hook.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import List, final 3 | 4 | from tw_hooks.base_hooks.base_hook import BaseHook 5 | from tw_hooks.types import TaskT 6 | from tw_hooks.utils import stdin_lines_to_json 7 | 8 | 9 | class OnExitHook(BaseHook): 10 | """On exit hook base class.""" 11 | 12 | @final 13 | def on_exit(self, stdin_lines: List[str]): 14 | """Entrypoint - to be called by the Hook shim.""" 15 | items = stdin_lines_to_json(stdin_lines) 16 | return self._on_exit(items) 17 | 18 | @abstractmethod 19 | def _on_exit(self, added_modified_tasks: List[TaskT]): 20 | """Implement this in your hook.""" 21 | 22 | @classmethod 23 | def entrypoint(cls) -> str: 24 | return "on_exit" 25 | 26 | @classmethod 27 | def require_stdin(cls) -> bool: 28 | return True 29 | 30 | @classmethod 31 | def shim_prefix(cls) -> str: 32 | return "on-exit" 33 | -------------------------------------------------------------------------------- /tw_hooks/base_hooks/on_modify_hook.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import List, final 3 | 4 | from tw_hooks.base_hooks.base_hook import BaseHook 5 | from tw_hooks.types import TaskT 6 | from tw_hooks.utils import stdin_lines_to_json 7 | 8 | 9 | class OnModifyHook(BaseHook): 10 | """On modify hook base class.""" 11 | 12 | @final 13 | def on_modify(self, stdin_lines: List[str]): 14 | """Entrypoint - to be called by the Hook shim.""" 15 | o, m = stdin_lines_to_json(stdin_lines) 16 | return self._on_modify(original_task=o, modified_task=m) 17 | 18 | @abstractmethod 19 | def _on_modify(self, original_task: TaskT, modified_task: TaskT): 20 | """Implement this in your hook.""" 21 | 22 | @classmethod 23 | def entrypoint(cls) -> str: 24 | return "on_modify" 25 | 26 | @classmethod 27 | def require_stdin(cls) -> bool: 28 | return True 29 | 30 | @classmethod 31 | def shim_prefix(cls) -> str: 32 | return "on-modify" 33 | -------------------------------------------------------------------------------- /tests/test_stdin_parsing.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | 6 | import tw_hooks.utils 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "stdlines", 11 | [f"stdlines{i}" for i in range(2)], 12 | indirect=True, 13 | ) 14 | def test_stdin_to_json(stdlines): 15 | tasks = tw_hooks.utils.stdin_lines_to_json(stdlines) 16 | assert len(tasks) == 2 17 | for i in range(2): 18 | assert isinstance(tasks[i], dict) 19 | assert set({"description", "due", "entry"}).issubset(tasks[i].keys()) 20 | 21 | 22 | TEST_DATA = Path(__file__).absolute().parent / "test_data" 23 | 24 | 25 | def test_parse_stdin_lines(): 26 | """Make sure we can parse stdin lines that contain non-unicode characters.""" 27 | f = open(TEST_DATA / "invalid_unicode_chars.txt", "r") 28 | with patch("tw_hooks.utils.sys.stdin.fileno", lambda: f.fileno()): 29 | tasks = tw_hooks.utils.parse_stdin_lines() 30 | assert len(tasks) == 2 31 | assert "Fasting" in tasks[0] 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nikos Koukis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tw_hooks/utils.py: -------------------------------------------------------------------------------- 1 | """Utility methods and fucntions. 2 | 3 | If something is generic enough, make a PR to 4 | """ 5 | import json 6 | import os 7 | import sys 8 | from json.decoder import JSONDecodeError 9 | from typing import List, Optional, Union, cast 10 | 11 | from bubop import logger 12 | 13 | from tw_hooks.types import ListOfTagsList, MapOfTags, TaskT 14 | 15 | 16 | def _use_json(json_str: str): 17 | if json_str == "": 18 | return {} 19 | else: 20 | try: 21 | out = json.loads(json_str) 22 | except JSONDecodeError: 23 | out = json.loads(json_str.replace("'", '"')) 24 | 25 | return out 26 | 27 | 28 | def stdin_lines_to_json(stdin_lines: List[str]) -> List[TaskT]: 29 | """ 30 | Parse all the lines from stdin and return them as a list of strings, each one 31 | corresponding to a single task. 32 | """ 33 | out: List[TaskT] = [] 34 | for line in stdin_lines: 35 | out.append(_use_json(line.strip())) 36 | 37 | return out 38 | 39 | 40 | _JsonRetValue = Optional[Union[MapOfTags, ListOfTagsList]] 41 | 42 | 43 | def get_json_from_environ(envvar: str) -> _JsonRetValue: 44 | """Parse an environment variable assuming it contains JSON.""" 45 | val = os.environ.get(envvar) 46 | if val is None: 47 | return None 48 | else: 49 | val = json.loads(val.replace("'", '"')) 50 | return cast(_JsonRetValue, val) 51 | 52 | 53 | def parse_stdin_lines() -> List[str]: 54 | with open(sys.stdin.fileno(), "r", encoding="utf-8", errors="ignore") as f: 55 | return f.readlines() 56 | -------------------------------------------------------------------------------- /tw_hooks/hooks/correct_tag_names.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import cast 3 | 4 | from tw_hooks import OnModifyHook 5 | from tw_hooks.base_hooks.on_add_hook import OnAddHook 6 | from tw_hooks.types import MapOfTags, TaskT 7 | from tw_hooks.utils import get_json_from_environ 8 | 9 | envvar = "TW_CORRECT_TAG_MAPPINGS" 10 | 11 | 12 | class CorrectTagNames(OnModifyHook, OnAddHook): 13 | """Change tag names based on a predefined lookup table (Use TW_TAG_MAPPINGS envvar). 14 | 15 | To specify a mapping, add something like this to your shell rc file: 16 | 17 | TW_CORRECT_TAG_MAPPINGS='{"movies": "movie", "wor": "work"}' 18 | """ 19 | 20 | def __init__(self, tag_mappings=get_json_from_environ(envvar)): 21 | if tag_mappings is None: 22 | tag_mappings = {} 23 | self._tag_mappings = cast(MapOfTags, tag_mappings) 24 | 25 | def _correct_tags(self, task: TaskT): 26 | if "tags" not in task: 27 | return 28 | 29 | tags = task["tags"] 30 | for bad_tag in self._tag_mappings.keys(): 31 | try: 32 | bad_idx = tags.index(bad_tag) 33 | tags[bad_idx] = self._tag_mappings[bad_tag] 34 | self.log(f"Correcting tag: {bad_tag} -> {self._tag_mappings[bad_tag]}") 35 | except ValueError: 36 | pass 37 | 38 | def _on_modify(self, original_task: TaskT, modified_task: TaskT): 39 | del original_task 40 | self._correct_tags(modified_task) 41 | print(json.dumps(modified_task)) 42 | 43 | def _on_add(self, added_task: TaskT): 44 | self._correct_tags(added_task) 45 | print(json.dumps(added_task)) 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # vim 105 | Session.vim 106 | -------------------------------------------------------------------------------- /tw_hooks/hooks/auto_tag_based_on_tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from typing import cast 4 | 5 | from tw_hooks import OnAddHook, OnModifyHook 6 | from tw_hooks.types import MapOfTags, TaskT 7 | from tw_hooks.utils import get_json_from_environ 8 | 9 | envvar = "TW_AUTO_TAG_MAPPINGS" 10 | 11 | 12 | class AutoTagBasedOnTags(OnModifyHook, OnAddHook): 13 | """ 14 | Inspect the list of tags in the added/modified tasks provided and add additional tags if required. 15 | 16 | To specify a mapping, add something like this to your shell rc file: 17 | 18 | TW_AUTO_TAG_MAPPINGS='{"python": "programming", "cpp": "programming", "github.*": "programming"}' 19 | """ 20 | 21 | def __init__(self, tag_mappings=get_json_from_environ(envvar)): 22 | if tag_mappings is None: 23 | tag_mappings = {} 24 | self._tag_mappings = cast(MapOfTags, tag_mappings) 25 | 26 | def _check_and_apply_extra_tags(self, task: TaskT): 27 | if "tags" not in task: 28 | return 29 | 30 | tags = task["tags"] 31 | old_tags = list(tags) 32 | for tag_pattern in self._tag_mappings.keys(): 33 | if not any(re.match(tag_pattern, tag) for tag in tags): 34 | continue 35 | 36 | new_tags = self._tag_mappings[tag_pattern] 37 | tags.extend(new_tags) 38 | if set(old_tags) != set(tags): 39 | self.log(f"Applying extra tags (due to pattern {tag_pattern}): {new_tags}") 40 | 41 | def _on_modify(self, original_task: TaskT, modified_task: TaskT): 42 | del original_task 43 | self._check_and_apply_extra_tags(modified_task) 44 | print(json.dumps(modified_task)) 45 | 46 | def _on_add(self, added_task: TaskT): 47 | self._check_and_apply_extra_tags(added_task) 48 | print(json.dumps(added_task)) 49 | -------------------------------------------------------------------------------- /tw_hooks/base_hooks/base_hook.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from bubop.string import camel_case_to_dashed 4 | 5 | 6 | class BaseHook(ABC): 7 | """Base class for all the Taskwarrior hooks.""" 8 | 9 | @classmethod 10 | @abstractmethod 11 | def shim_prefix(cls) -> str: 12 | """Return the prefix that is to be used when creating shims for this type of hooks. 13 | 14 | Implement in the direct children only. 15 | """ 16 | 17 | @classmethod 18 | @abstractmethod 19 | def entrypoint(cls) -> str: 20 | """Name of the method that is meant to be the entrypoint for this hook.""" 21 | 22 | @classmethod 23 | @abstractmethod 24 | def require_stdin(cls) -> bool: 25 | """True if this Hook requires access to the standard input.""" 26 | 27 | @classmethod 28 | def _get_subclass_name(cls) -> str: 29 | return cls.__name__ 30 | 31 | @classmethod 32 | def description(cls) -> str: 33 | """Get a description of this class based on the first line of its docstring.""" 34 | doc = cls.__doc__ 35 | if not doc: 36 | doc = "No description" 37 | else: 38 | doc = doc.strip("\n ").split("\n")[0] 39 | 40 | return doc 41 | 42 | @classmethod 43 | def name(cls) -> str: 44 | """Return the name of the child class.""" 45 | return cls._get_subclass_name() 46 | 47 | @classmethod 48 | def dashed_name(cls) -> str: 49 | """Return the name of the child class in dashed format - instead of camel-case.""" 50 | name = cls.name() 51 | return camel_case_to_dashed(name) 52 | 53 | def __str__(self) -> str: 54 | """Return in string form.""" 55 | return self.name() 56 | 57 | def log(self, s: str): 58 | """Helper method for the child classes to log and prefix it with their name.""" 59 | print(f"[{self.name()}] {s}") 60 | -------------------------------------------------------------------------------- /tw_hooks/hooks/detect_mutually_exclusive_tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List, Set, cast 3 | 4 | from tw_hooks import OnModifyHook 5 | from tw_hooks.base_hooks.on_add_hook import OnAddHook 6 | from tw_hooks.types import ListOfTagsList, Retcode, TaskT 7 | from tw_hooks.utils import get_json_from_environ 8 | 9 | envvar = "TW_INCOMPATIBLE_TAG_SETS" 10 | 11 | 12 | class DetectMutuallyExclusiveTags(OnModifyHook, OnAddHook): 13 | """ 14 | Inspect the list of tags in the added/modified tasks and see whether the user has specified an incompatible combination of tags. 15 | 16 | To specify a mapping of tags that don't work well with each other, add a variable like the 17 | following in your shell RC file: 18 | 19 | TW_INCOMPATIBLE_TAG_SETS='[("projectideas", "freetime")]' 20 | """ 21 | 22 | def __init__(self, tag_sets=get_json_from_environ(envvar)): 23 | if tag_sets is None: 24 | tag_sets = [] 25 | if not isinstance(tag_sets, list): 26 | raise RuntimeError( 27 | f"Parsed value from {envvar} doesn't contain a list as expected but a" 28 | f" {type(tag_sets)}-> {tag_sets}" 29 | ) 30 | self._tag_sets: List[Set[str]] = [set(li) for li in cast(ListOfTagsList, tag_sets)] 31 | 32 | def _detect_incompatible_tags(self, task: TaskT) -> Retcode: 33 | if "tags" not in task: 34 | return 0 35 | 36 | tags = task["tags"] 37 | for tag_set in self._tag_sets: 38 | if tag_set.issubset(tags): 39 | self.log(f"Can't use the following tags together -> {tag_set}") 40 | return 1 41 | 42 | return 0 43 | 44 | def _on_modify(self, original_task: TaskT, modified_task: TaskT) -> Retcode: 45 | del original_task 46 | ret = self._detect_incompatible_tags(modified_task) 47 | print(json.dumps(modified_task)) 48 | return ret 49 | 50 | def _on_add(self, added_task: TaskT) -> Retcode: 51 | ret = self._detect_incompatible_tags(added_task) 52 | print(json.dumps(added_task)) 53 | return ret 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | install_with_pip3: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-python@v2 13 | with: 14 | python-version: "3.8" 15 | - name: Install with pip3 16 | run: | 17 | pip3 install . 18 | pip3 show tw_hooks 19 | unittests: 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | fail-fast: true 23 | matrix: 24 | os: [ubuntu-latest] 25 | python-version: ["3.8", "3.9"] 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Python Poetry Action 33 | uses: abatilo/actions-poetry@v2.1.3 34 | - name: Install prerequisites 35 | run: poetry install 36 | - name: Run tests 37 | run: poetry run pytest --doctest-modules 38 | style_and_linters: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions/setup-python@v2 43 | with: 44 | python-version: "3.8" 45 | - name: Python Poetry Action 46 | uses: abatilo/actions-poetry@v2.1.3 47 | - name: Install prerequisites 48 | run: poetry install 49 | - name: Run style checkers and linters 50 | run: poetry run pre-commit run --all-files 51 | coverage: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions/setup-python@v2 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | - name: Python Poetry Action 59 | uses: abatilo/actions-poetry@v2.1.3 60 | - name: Install prerequisites 61 | run: poetry install 62 | - name: Coverage 63 | run: | 64 | poetry run coverage run -m pytest --doctest-modules 65 | poetry run coverage report 66 | - name: Coveralls 67 | run: poetry run coveralls 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 71 | 72 | publish_package: 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: actions/checkout@v2 76 | - name: Publish package to pypi 77 | uses: JRubics/poetry-publish@v1.9 78 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 79 | with: 80 | pypi_token: ${{ secrets.PYPI_API_TOKEN }} 81 | -------------------------------------------------------------------------------- /tw_hooks/hooks/warn_on_task_congestion.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime, timedelta 3 | from pathlib import Path 4 | from typing import Union 5 | 6 | from tw_hooks.base_hooks.on_exit_hook import OnExitHook 7 | 8 | 9 | class WarnOnTaskCongestion(OnExitHook): 10 | """ 11 | Warn the user if there are too many tasks. 12 | 13 | By default this class will warn if there are multiple tasks that are due:today. 14 | 15 | In the future the date field (due, scheduled) that will be used for this should be 16 | configurable. 17 | """ 18 | 19 | def __init__( 20 | self, 21 | task_dir: Union[str, Path] = Path.home() / ".task", 22 | date_field="due", 23 | warn_threshold=20, 24 | ): 25 | self._task_dir = Path(task_dir) 26 | self._pending_data = self._task_dir / "pending.data" 27 | 28 | self._date_field = date_field 29 | self._date = "today" 30 | self._re_pat = rf"({self._date_field}:\"\d+).*" + "]" 31 | self._warn_threshold = warn_threshold 32 | 33 | def _on_exit(self, _): 34 | # I can't just invoke the taskwarrior executable. There's some sort of lock being 35 | # acquired so a potential subprocess.run call is blocking forever. 36 | # I have to manually parse pending.data 37 | if not self._pending_data.is_file(): 38 | self.log(f"Can't find pending.data file -> {self._pending_data}") 39 | return 1 40 | 41 | # compute range of timestamps which corresponds to "today" 42 | today = datetime.today() 43 | today = datetime(year=today.year, month=today.month, day=today.day) 44 | today_start = int(today.timestamp()) 45 | tomorrow_start = int((today + timedelta(days=1)).timestamp()) 46 | 47 | conts = self._pending_data.read_text(errors="ignore") 48 | filter_ = f"{self._date_field}:{self._date}" 49 | re_iter = re.finditer(pattern=self._re_pat, string=conts) 50 | 51 | # count all the tasks whose selected date is today. If that surpasses our threshold 52 | # break and issue warning 53 | count = 0 54 | for g in re_iter: 55 | g_ = g.group(1) 56 | ts = int(g_.split('"')[-1]) 57 | if today_start <= ts < tomorrow_start: 58 | count += 1 59 | if count > self._warn_threshold: 60 | self.log( 61 | f"Too many {filter_} tasks (threshold={self._warn_threshold})." 62 | " Consider reducing them to avoid noise in your reports" 63 | ) 64 | break 65 | return 0 66 | -------------------------------------------------------------------------------- /tests/test_correct_tag_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | from pytest import fixture 4 | 5 | from tw_hooks.base_hooks.on_modify_hook import OnModifyHook 6 | from tw_hooks.hooks.correct_tag_names import CorrectTagNames 7 | from tw_hooks.utils import _use_json 8 | 9 | 10 | @fixture 11 | def hook0() -> OnModifyHook: 12 | tag_mappings = {"this": "that", "something": "else"} 13 | c = CorrectTagNames(tag_mappings=tag_mappings) 14 | return c 15 | 16 | 17 | @fixture 18 | def hook1() -> OnModifyHook: 19 | tag_mappings = {"movie": "movies"} 20 | c = CorrectTagNames(tag_mappings=tag_mappings) 21 | return c 22 | 23 | 24 | @fixture 25 | def hook2() -> OnModifyHook: 26 | tag_mappings = {"movie": "movies", "wor": "work"} 27 | c = CorrectTagNames(tag_mappings=tag_mappings) 28 | return c 29 | 30 | 31 | def test_nop( 32 | on_modify_changed_title: List[str], 33 | on_modify_changed_title_mod_dict: Dict[str, Any], 34 | capsys, 35 | hook0: OnModifyHook, 36 | ): 37 | """If the tags are irrelevant then this hook should do nothing.""" 38 | hook0.on_modify(on_modify_changed_title) 39 | captured = capsys.readouterr() 40 | assert _use_json(captured.out.strip()) == on_modify_changed_title_mod_dict 41 | assert captured.err == "" 42 | 43 | 44 | def test_change_one_tag( 45 | on_modify_changed_title: List[str], 46 | on_modify_changed_title_mod_dict: Dict[str, Any], 47 | capsys, 48 | hook1: OnModifyHook, 49 | ): 50 | """If one tag was identified, change that, leave the rest unchanged.""" 51 | hook1.on_modify(on_modify_changed_title) 52 | captured = capsys.readouterr() 53 | on_modify_changed_title_mod_dict["tags"] = ["movies", "wor"] 54 | out: str = captured.out 55 | parts = out.split("\n", maxsplit=1) 56 | assert parts[0].startswith("[CorrectTagNames] Correcting") 57 | assert _use_json(parts[1].strip()) == on_modify_changed_title_mod_dict 58 | assert captured.err == "" 59 | 60 | 61 | def test_change_multiple_tag( 62 | on_modify_changed_title: List[str], 63 | on_modify_changed_title_mod_dict: Dict[str, Any], 64 | capsys, 65 | hook2: OnModifyHook, 66 | ): 67 | """ 68 | If multiple tags were identified, make sure all were changed correctly to the right values. 69 | """ 70 | hook2.on_modify(on_modify_changed_title) 71 | captured = capsys.readouterr() 72 | on_modify_changed_title_mod_dict["tags"] = ["movies", "work"] 73 | out: str = captured.out 74 | parts = out.split("\n", maxsplit=2) 75 | assert parts[0].startswith("[CorrectTagNames] Correcting") 76 | assert parts[1].startswith("[CorrectTagNames] Correcting") 77 | assert _use_json(parts[2].strip()) == on_modify_changed_title_mod_dict 78 | 79 | assert captured.err == "" 80 | -------------------------------------------------------------------------------- /tw_hooks/hooks/post_latest_start_to_i3_status.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | 5 | from tw_hooks import OnModifyHook 6 | from tw_hooks.types import Retcode, TaskT 7 | 8 | envvar = "TW_I3STATUS_RS_DBUS_NAME" 9 | 10 | 11 | class PostLatestSTartToI3Status(OnModifyHook): 12 | """When a task is started, send the title of the task to i3status-rs via DBus. 13 | 14 | This is in order to update a custom DBus block in the toolbar. 15 | 16 | To specify the name with which to communicate with i3status-rs over DBus, set the following 17 | environment variable in your shell RC file: 18 | 19 | TW_I3STATUS_RS_DBUS_NAME=ActiveTaskwarriorTask 20 | """ 21 | 22 | def __init__(self, dbus_name=os.environ.get(envvar)): 23 | self._dbus_name: str = dbus_name if dbus_name else "" 24 | 25 | def _detect_start_of_task(self, task: TaskT) -> bool: 26 | """Return True if task is marked as started, false otherwise 27 | 28 | I don't care whether the task was alredy started or not. 29 | """ 30 | return "start" in task.keys() 31 | 32 | def _post_to_dbus(self, task) -> Retcode: 33 | task_desc = f'{task["uuid"][:8]} | {task["description"]}' 34 | if "annotations" in task: 35 | annotations = " | ".join( 36 | annotation_dict["description"][:50] for annotation_dict in task["annotations"] 37 | ) 38 | task_desc += f" | {annotations}" 39 | 40 | # upper limit on the length of the string I'll be sending 41 | task_desc = task_desc[:200] 42 | 43 | proc = subprocess.run( 44 | [ 45 | "busctl", 46 | "--user", 47 | "call", 48 | "i3.status.rs", 49 | "/ActiveTaskwarriorTask", 50 | "i3.status.rs", 51 | "SetStatus", 52 | "sss", 53 | task_desc, 54 | "tasks", 55 | "Good", 56 | ], 57 | check=False, 58 | ) 59 | 60 | if proc.returncode != 0: 61 | # Don't fail this execution. Updating the i3status is not that important. 62 | if proc.stdout: 63 | out = proc.stdout.decode("utf-8") 64 | else: 65 | out = "" 66 | if proc.stderr: 67 | err = proc.stderr.decode("utf-8") 68 | else: 69 | err = "" 70 | self.log(f"Failed to send started task.\n\nstdout: {out}\n\nstderr: {err}") 71 | 72 | return 0 73 | 74 | def _on_modify(self, original_task: TaskT, modified_task: TaskT): 75 | del original_task 76 | ret = 0 77 | if self._detect_start_of_task(modified_task): 78 | ret = self._post_to_dbus(modified_task) 79 | 80 | print(json.dumps(modified_task)) 81 | return ret 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## Development 4 | 5 | - We're using [poetry](https://python-poetry.org/docs/) for building and 6 | packaging of this Python project. You can find installation instructions 7 | [here](https://python-poetry.org/docs/#installation). 8 | 9 | - We're using `pytest` for testing the code and a variety of plugins for linting 10 | and checking for bugs, including: 11 | 12 | - [mypy](http://mypy-lang.org/) 13 | - [pyright](https://github.com/Microsoft/pyright) 14 | - [pycln](https://hadialqattan.github.io/pycln/) 15 | - [isort](https://pypi.org/project/isort/) 16 | - [pre-commit](https://pre-commit.com/) 17 | 18 | All these development dependencies will be available inside the virtual 19 | environment that poetry sets up. You can get a prompt inside the said virtual 20 | environment by running `poetry shell` while at the root of this repo. 21 | 22 | ```sh 23 | # install dependencies and all extras - which are required for running the 24 | # tests. 25 | poetry install -E google -E gkeep -E notion 26 | 27 | # get a shell inside the virtualenv 28 | poetry shell 29 | ``` 30 | 31 | To run all the linters in one go you can use `pre-commit`. To do this: 32 | 33 | 1. Setup the `virtulenv` with `poetry` as described above. 34 | 1. Get a shell inside the virtualenv. Install the `pre-commit` hook. 35 | 36 | ```sh 37 | pre-commit install 38 | ``` 39 | 40 | 1. While inside this `virtualenv` you can run `pre-commit run --all-files` to 41 | run all the linting/formatting checks (excluding the tests). 42 | 1. The linting and formatting programs will run on all files that changed on 43 | every new commit automatically. 44 | 45 | - To run the tests just run `pytest`. The pytest configuration for this project 46 | (as well as the configuration for each one of the tools can be found in the 47 | `pyproject.toml` file. At the time of writing this: 48 | 49 | ```toml 50 | # pytest ----------------------------------------------------------------------- 51 | [tool.pytest.ini_options] 52 | addopts = ["--ignore-glob=quickstart*", "--doctest-modules"] 53 | ``` 54 | 55 | ## Git Guidelines 56 | 57 | - Make sure that the branch from which you're making a Pull Request is rebased 58 | on top of the branch you're making the PR to. 59 | - In terms of the commit message: 60 | 61 | - Start the message header with a verb 62 | - Capitalise the first word (the verb mentioned above). 63 | - Format your commit messages in imperative form 64 | - If the pull-request is referring to a particular `Side`, prefix it with 65 | `[side-name]` 66 | 67 | For example... 68 | 69 | ```gitcommit 70 | # ❌ don't do... 71 | implemented feature A for google calendar 72 | # ✅ instead do... 73 | [gcal] Implement feature A 74 | # ✅ if this is about a synchronization that's about two sides, join them by 75 | # a dash... 76 | [tw-gcal] Fix regression in Taskwarrior - Google Keep todo blocks integration. 77 | ``` 78 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | import pytest 4 | from pytest import fixture 5 | 6 | from tw_hooks.types import TaskT 7 | 8 | 9 | @fixture 10 | def on_modify_changed_title( 11 | on_modify_changed_title_orig, on_modify_changed_title_mod 12 | ) -> List[str]: 13 | return [on_modify_changed_title_orig, on_modify_changed_title_mod] 14 | 15 | 16 | @fixture 17 | def on_modify_changed_title_orig(on_modify_changed_title_orig_dict) -> str: 18 | return f"{on_modify_changed_title_orig_dict}\n" 19 | 20 | 21 | @fixture 22 | def on_modify_changed_title_mod(on_modify_changed_title_mod_dict) -> str: 23 | return f"{on_modify_changed_title_mod_dict}\n" 24 | 25 | 26 | @fixture 27 | def on_modify_changed_title_orig_dict() -> TaskT: 28 | return { 29 | "description": "kalimera kalimera kalimera", 30 | "entry": "20220602T212503Z", 31 | "modified": "20220602T212709Z", 32 | "status": "pending", 33 | "uuid": "c236dff8-76bb-4a8c-a075-0657c633c018", 34 | "tags": ["movie"], 35 | } 36 | 37 | 38 | @fixture 39 | def on_modify_changed_title_mod_dict() -> TaskT: 40 | return { 41 | "description": "kalimera kalimera kalimera kalimera", 42 | "entry": "20220602T212503Z", 43 | "modified": "20220602T212709Z", 44 | "status": "pending", 45 | "uuid": "c236dff8-76bb-4a8c-a075-0657c633c018", 46 | "tags": ["movie", "wor"], 47 | } 48 | 49 | 50 | @pytest.fixture() 51 | def stdlines(request: pytest.FixtureRequest): 52 | """Fixture to parametrize on.""" 53 | param = request.param # type: ignore 54 | return request.getfixturevalue(param) # type: ignore 55 | 56 | 57 | @fixture 58 | def stdlines0() -> List[Any]: 59 | return [ 60 | '{"description":"Meditate","due":"20220528T155959Z","entry":"20220528T144107Z","mask":"XXXXX-WW","modified":"20220603T161607Z","recur":"1d","rtype":"periodic","status":"recurring","uuid":"0fbe94ed-0b8b-419b-ab49-d85acd6c8b8e","wait":"20220527T225959Z","tags":["remindme","routine"]}\n', 61 | '{"description":"Meditate","due":"20220528T155959Z","entry":"20220528T144107Z","mask":"XXXXXXWW","modified":"20220603T161607Z","recur":"1d","rtype":"periodic","status":"recurring","uuid":"0fbe94ed-0b8b-419b-ab49-d85acd6c8b8e","wait":"20220527T225959Z","tags":["remindme","routine"]}\n', 62 | ] 63 | 64 | 65 | @fixture 66 | def stdlines1() -> List[Any]: 67 | return [ 68 | '{"description":"🍣' 69 | ' Food","due":"20220528T155959Z","entry":"20220528T144107Z","mask":"XXXXX-WW","modified":"20220603T161607Z","recur":"1d","rtype":"periodic","status":"recurring","uuid":"0fbe94ed-0b8b-419b-ab49-d85acd6c8b8e","wait":"20220527T225959Z","tags":["remindme","routine"]}\n', 70 | '{"description":"🍣🍣🍣 More' 71 | ' food","due":"20220528T155959Z","entry":"20220528T144107Z","mask":"XXXXXXWW","modified":"20220603T161607Z","recur":"1d","rtype":"periodic","status":"recurring","uuid":"0fbe94ed-0b8b-419b-ab49-d85acd6c8b8e","wait":"20220527T225959Z","tags":["remindme","routine"]}\n', 72 | ] 73 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: "v3.2.0" 6 | hooks: 7 | - id: check-added-large-files 8 | - id: check-ast 9 | - id: check-merge-conflict 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: debug-statements 13 | - id: detect-private-key 14 | - id: end-of-file-fixer 15 | exclude: ".*svg" 16 | - id: mixed-line-ending 17 | - id: trailing-whitespace 18 | # - id: check-symlinks 19 | # - id: pretty-format-json 20 | # - id: sort-simple-yaml 21 | - repo: https://github.com/tcort/markdown-link-check 22 | rev: "v3.10.0" 23 | hooks: 24 | # https://github.com/tcort/markdown-link-check#config-file-format 25 | - id: markdown-link-check 26 | args: ["--config", ".markdown-link-check.json"] 27 | - repo: https://github.com/markdownlint/markdownlint 28 | rev: "v0.11.0" 29 | hooks: 30 | - id: markdownlint 31 | exclude: ^.github/ 32 | entry: mdl -r ~MD033,~MD013 33 | - repo: local 34 | hooks: 35 | - id: check-github-workflows 36 | name: Validate GitHub Workflows 37 | description: 'Validate GitHub Workflows against the schema provided by SchemaStore' 38 | entry: check-jsonschema --builtin-schema vendor.github-workflows 39 | language: python 40 | files: ^\.github/workflows/ 41 | types: [yaml] 42 | - id: black 43 | name: Black formatter 44 | description: "Black: The uncompromising Python code formatter" 45 | entry: black 46 | language: system 47 | minimum_pre_commit_version: 2.9.2 48 | require_serial: true 49 | types_or: [python, pyi] 50 | - id: pycln 51 | name: Pyclean - Find redundant imports 52 | entry: pycln 53 | language: system 54 | pass_filenames: true 55 | types_or: [python, pyi] 56 | - id: mypy 57 | name: Mypy check 58 | entry: mypy 59 | language: system 60 | pass_filenames: true 61 | types_or: [python, pyi] 62 | - id: pyright 63 | name: Pyright check 64 | entry: pyright 65 | language: system 66 | pass_filenames: true 67 | types_or: [python, pyi] 68 | # - id: Pylint check 69 | # name: pylint 70 | # entry: pylint 71 | # language: system 72 | # pass_filenames: true 73 | # types: [python] 74 | # exclude: ^tests/ 75 | - id: isort 76 | name: Isort check 77 | entry: isort 78 | language: system 79 | pass_filenames: true 80 | types: [python] 81 | - id: check-poetry 82 | name: Poetry check 83 | description: Validates the structure of the pyproject.toml file 84 | entry: poetry check 85 | language: system 86 | pass_filenames: false 87 | files: pyproject.toml 88 | - repo: meta 89 | hooks: 90 | - id: check-hooks-apply 91 | - id: check-useless-excludes 92 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tw_hooks" 3 | version = "0.1.6" 4 | description = "Collection of Taskwarrior hooks" 5 | authors = ["Nikos Koukis "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/bergercookie/tw-hooks" 9 | repository = "https://github.com/bergercookie/tw-hooks" 10 | include = ["tw_hooks/py.typed"] 11 | classifiers = [ 12 | "Framework :: Pytest", 13 | "License :: OSI Approved :: MIT License", 14 | "Natural Language :: English", 15 | "Operating System :: Unix", 16 | "Programming Language :: Python :: 3 :: Only", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python", 21 | "Topic :: Utilities", 22 | ] 23 | keywords = [] 24 | 25 | # end-user dependencies -------------------------------------------------------- 26 | [tool.poetry.dependencies] 27 | python = "^3.8" 28 | bubop = "0.1.8" 29 | 30 | [tool.poetry.dev-dependencies] 31 | pytest = "*" 32 | black = { version = "*", allow-prereleases = true } 33 | isort = "*" 34 | mock = "*" 35 | pdbpp = "^0.10.3" 36 | identify = "^2.4.0" 37 | mypy = "*" 38 | pre-commit = "^2.16.0" 39 | pyfakefs = "^4.5.3" 40 | pylint = "^2.12.2" 41 | pyright = "*" 42 | types-PyYAML = "^6.0.1" 43 | types-python-dateutil = "^2.8.3" 44 | 45 | coverage = "^6.2" 46 | coveralls = "^3.3.1" 47 | pycln = "^1.3.1" 48 | check-jsonschema = "^0.14.3" 49 | 50 | [tool.poetry.scripts] 51 | install-hook-shims = "tw_hooks.scripts.install_hook_shims:main" 52 | 53 | # isort ------------------------------------------------------------------------ 54 | [tool.isort] 55 | include_trailing_comma = true 56 | line_length = 95 57 | multi_line_output = 3 58 | profile = "black" 59 | 60 | # black ------------------------------------------------------------------------ 61 | [tool.black] 62 | preview = true 63 | line-length = 95 64 | target-version = ['py38', 'py39'] 65 | include = '\.pyi?$' 66 | 67 | # mypy ------------------------------------------------------------------------- 68 | [tool.mypy] 69 | warn_return_any = true 70 | warn_unused_configs = true 71 | no_implicit_reexport = false 72 | 73 | [[tool.mypy.overrides]] 74 | module = ["tqdm", "pytest", "pexpect"] 75 | ignore_missing_imports = true 76 | 77 | # pylint ----------------------------------------------------------------------- 78 | [tool.pylint] 79 | [tool.pylint.master] 80 | persistent = "yes" 81 | suggestion-mode = "yes" 82 | unsafe-load-any-extension = "no" 83 | ignore = "VCS" 84 | 85 | [tool.pylint.messages_control] 86 | disable = "C0103,W0613,R1720,R1705,W0104,C0301" 87 | 88 | [tool.pylint.refactoring] 89 | max-nested-blocks = 5 90 | never-returning-functions = "sys.exit,argparse.parse_error" 91 | 92 | [tool.pylint.similarities] 93 | ignore-comments = "yes" 94 | ignore-docstrings = "no" 95 | ignore-imports = "yes" 96 | # ignore-signatures=no 97 | min-similarity-lines = 4 98 | 99 | [tool.pylint.format] 100 | max-line-length = 95 101 | ignore-invalid-name = true 102 | max-module-lines = 500 103 | 104 | [tool.pylint.string] 105 | check-quote-consistency = "yes" 106 | 107 | # pyright ---------------------------------------------------------------------- 108 | [tool.pyright] 109 | reportMissingImports = true 110 | reportMissingTypeStubs = false 111 | pythonVersion = "3.8" 112 | pythonPlatform = "Linux" 113 | 114 | # pytest ----------------------------------------------------------------------- 115 | [tool.pytest.ini_options] 116 | addopts = "--doctest-modules" 117 | 118 | # build-system ----------------------------------------------------------------- 119 | [build-system] 120 | requires = ["poetry>=0.12"] 121 | build-backend = "poetry.masonry.api" 122 | 123 | # vim: tw=80 124 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | nickkouk@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | . Translations are available at 128 | . 129 | -------------------------------------------------------------------------------- /tw_hooks/scripts/install_hook_shims.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Detect Taskwarrior hooks and register an executable shim for each one of them.""" 3 | import sys 4 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 5 | from importlib import import_module 6 | from pathlib import Path 7 | from typing import Dict, Sequence, Set, Type 8 | 9 | from bubop.classes import all_subclasses 10 | from bubop.fs import valid_path 11 | from bubop.logging import logger 12 | from bubop.string import format_dict, format_list 13 | 14 | from tw_hooks.base_hooks import BaseHook, OnAddHook, OnExitHook, OnLaunchHook, OnModifyHook 15 | from tw_hooks.hooks import import_concrete_hooks 16 | 17 | # By default this will find all the implementations in this package. If the user wants to 18 | # include their own hook classes, they'll have to specify the module paths in the 19 | # TW_ADDITIONAL_HOOKS environment variable 20 | 21 | 22 | HOOK_TEMPLATE = """ 23 | #!/usr/bin/env python3 24 | 25 | import sys 26 | from pathlib import Path 27 | # Make this robust in case e.g., the user is running inside a virtualenv and tw_hooks is not 28 | # installed in there. 29 | try: 30 | from {import_from} import {class_name} 31 | except ModuleNotFoundError: 32 | print("Can't import {class_name} hook") 33 | 34 | # We have to return some JSON that's compatible with the hooks API 35 | # https://taskwarrior.org/docs/hooks/ 36 | name = Path(__file__).name 37 | hook_type = "-".join(name.split("-")[:2]) 38 | 39 | stdin = sys.stdin.read().strip() 40 | 41 | if hook_type == "on-add": 42 | added_task = stdin 43 | print(added_task) 44 | elif hook_type == "on-modify": 45 | modified_task = stdin.splitlines()[-1] 46 | print(modified_task) 47 | else: 48 | pass 49 | 50 | sys.exit(0) 51 | 52 | obj = {class_name}() 53 | {invoke_instructions} 54 | """ 55 | 56 | INVOKE_WITH_STDIN_TEMPLATE = """ 57 | from tw_hooks.utils import parse_stdin_lines 58 | sys.exit(obj.{class_entrypoint}(parse_stdin_lines())) 59 | """ 60 | 61 | INVOKE_WITH_NO_ARGS_TEMPLATE = """ 62 | sys.exit(obj.{class_entrypoint}()) 63 | """ 64 | 65 | 66 | def _build_shim(base_hook: Type[BaseHook], hook: Type[BaseHook]) -> str: 67 | invoke_instructions = ( 68 | INVOKE_WITH_STDIN_TEMPLATE 69 | if base_hook.require_stdin() 70 | else INVOKE_WITH_NO_ARGS_TEMPLATE 71 | ) 72 | invoke_instructions = invoke_instructions.format(class_entrypoint=base_hook.entrypoint()) 73 | return HOOK_TEMPLATE.format( 74 | import_from=hook.__module__, 75 | class_name=hook.name(), 76 | invoke_instructions=invoke_instructions, 77 | ).strip() 78 | 79 | 80 | def main(): 81 | """Main.""" 82 | # parse CLI arguments --------------------------------------------------------------------- 83 | task_dir_default = Path.home() / ".task" 84 | 85 | parser = ArgumentParser( 86 | __doc__, 87 | formatter_class=RawDescriptionHelpFormatter, 88 | ) 89 | parser.add_argument( 90 | "-t", 91 | "--task-dir", 92 | type=valid_path, 93 | help="Path to the taskwarrior main directory", 94 | default=task_dir_default, 95 | ) 96 | parser.add_argument( 97 | "-a", 98 | "--all-hooks", 99 | help="Install shims for all the hooks", 100 | action="store_true", 101 | ) 102 | parser.add_argument( 103 | "-l", 104 | "--list-hooks", 105 | help="List the available hooks and exit", 106 | action="store_true", 107 | ) 108 | parser.add_argument("-r", "--register-additional", nargs="+", default=[]) 109 | 110 | executable = Path(sys.argv[0]).stem 111 | usecases = { 112 | "Install only the WarnOnTaskCongestion hook (assuming you've installed tw_hooks with e.g., pip3)": ( 113 | "-r tw_hooks.hooks.warn_on_task_congestion" 114 | ), 115 | "Install all the available hooks from this repo (assuming you've installed tw_hooks with e.g., pip3)": ( 116 | "--all-hooks" 117 | ), 118 | 'Install a custom hook defined in .../dir/mod/hook_name.py. "dir" should be in your PYTHONPATH': ( 119 | "-r mod.hook_name" 120 | ), 121 | "List all the available hooks and exit": "--list-hooks", 122 | } 123 | parser.epilog = f'Usage examples:\n{"=" * 15}\n\n' + "\n".join( 124 | (f"- {k}\n {executable} {v}\n" for k, v in usecases.items()) 125 | ) 126 | 127 | args = vars(parser.parse_args()) 128 | additional_hook_modules: Sequence[str] = args["register_additional"] 129 | install_all_hooks: bool = args["all_hooks"] 130 | list_hooks: bool = args["list_hooks"] 131 | hooks_dir: Path = args["task_dir"] / "hooks" 132 | 133 | if (not install_all_hooks and not additional_hook_modules) and not list_hooks: 134 | raise RuntimeError( 135 | 'You have to specify at least one of the "--all-hooks", "--register-additional"' 136 | " options otherwise no hooks are going to be installed.\n" 137 | "Alternatively use --list-hooks to see the hooks that can be installed" 138 | ) 139 | 140 | if list_hooks: 141 | install_all_hooks = True 142 | 143 | # print CLI configuration ----------------------------------------------------------------- 144 | print( 145 | format_dict( 146 | header="CLI Configuration", 147 | items={ 148 | "Hooks directory": (hooks_dir), 149 | "Install all available hooks": install_all_hooks, 150 | "List hooks and exit": list_hooks, 151 | "Register hooks from modules": additional_hook_modules, 152 | }, 153 | align_items=True, 154 | ) 155 | ) 156 | 157 | if install_all_hooks: 158 | logger.info("Installing all available hooks...") 159 | import_concrete_hooks() 160 | 161 | # iterate over additional_hook_modules, try to find a (partial) match on ------------------ 162 | # any importable module and import it 163 | for ad_hook in additional_hook_modules: 164 | try: 165 | import_module(ad_hook) 166 | except ModuleNotFoundError: 167 | logger.error( 168 | f"Couldn't find module {ad_hook} in your path. Make sure this module is" 169 | " accessible, e.g., by adding its location in $PYTHONPATH" 170 | ) 171 | 172 | # initialize hooks directory -------------------------------------------------------------- 173 | if hooks_dir.exists(): 174 | if not hooks_dir.is_dir(): 175 | raise RuntimeError(f"{hooks_dir} exists and is not a directory, can't proceed.") 176 | 177 | else: 178 | logger.info("Hooks directory not present, creating it...") 179 | hooks_dir.mkdir(parents=True, exist_ok=False) 180 | 181 | # fetch all the hook implemenattions and group them per base hook ------------------------- 182 | hook_bases: Sequence[Type[BaseHook]] = [OnExitHook, OnLaunchHook, OnAddHook, OnModifyHook] 183 | hooks_to_install: Dict[Type[BaseHook], Set[Type[BaseHook]]] = {} 184 | 185 | # gather all the hooks -------------------------------------------------------------------- 186 | hook_with_descriptions: Dict[str, Sequence[str]] = {} # only for reporting to the user... 187 | for SomeBaseHook in hook_bases: 188 | subclasses: Set[Type[BaseHook]] = all_subclasses(SomeBaseHook) # type: ignore # TODO? 189 | hooks_to_install[SomeBaseHook] = subclasses 190 | hook_with_descriptions[SomeBaseHook.name()] = [ 191 | f"{Subclass.name()}: {Subclass.description()}" for Subclass in subclasses 192 | ] 193 | 194 | # report all the hook implementations found ----------------------------------------------- 195 | logger.info("Available hooks") 196 | for hook_name, descriptions in hook_with_descriptions.items(): 197 | print(format_list(descriptions, header=hook_name, indent=2)) 198 | if list_hooks: 199 | logger.info("Skipping installation of hooks...") 200 | return 201 | 202 | # make sure there are shims to install otherwise exit 203 | for _, subclasses in hooks_to_install.items(): 204 | if subclasses: 205 | break 206 | else: 207 | logger.warning("No shims to install.") 208 | return 209 | 210 | # install a shim under the hooks directory for each hook implementation ------------------- 211 | logger.info(f"Installing shim executables under {hooks_dir}") 212 | for SomeBaseHook, subclasses in hooks_to_install.items(): 213 | for SomeHook in subclasses: 214 | logger.debug(f"Creating shim for {SomeHook.name()}.{SomeBaseHook.entrypoint()}") 215 | shim_contents = _build_shim(SomeBaseHook, SomeHook) 216 | shim_path = hooks_dir / f"{SomeBaseHook.shim_prefix()}-{SomeHook.dashed_name()}.py" 217 | shim_path.write_text(shim_contents) 218 | shim_path.chmod(mode=0o764) 219 | 220 | return 221 | 222 | 223 | if __name__ == "__main__": 224 | main() 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taskwarrior Hooks 2 | 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 | pre-commit 11 | 12 | 13 | Coverage Status 14 | 15 | 16 | 17 | 18 | 19 | PyPI version 20 | 21 | Downloads 22 | 23 | Code style: black 24 | 25 | ## Description 26 | 27 | This is a collection of [Taskwarrior 28 | hooks](https://taskwarrior.org/docs/hooks_guide.html) that I use in my 29 | day-to-day workflows. It comes along a detection and easy-registration mechanism 30 | that should make it easy to develop and then distribute your own hooks. The 31 | hooks are structured as classes under the `tw_hooks/hooks` directory. 32 | 33 | ## Installation 34 | 35 | Install it from `PyPI`: 36 | 37 | ```sh 38 | pip3 install --user --upgrade tw_hooks 39 | ``` 40 | 41 | To get the latest version install directly from source: 42 | 43 | ```sh 44 | pip3 install --user --upgrade git+https://github.com/bergercookie/tw-hooks 45 | ``` 46 | 47 | After the installation, you have to run the `install-hooks-shims` executable 48 | (which by this point should be in your `$PATH`). Running it will create shims 49 | (thin wrapper scripts) under `~/.task/hooks` in order to register all the hooks 50 | with Taskwarrior. 51 | 52 | ## Available hooks 53 | 54 | Currently the following hooks are available out-of-the-box: 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
HookDescriptionEvents
AutoTagBasedOnTagsInspect the list of tags in the added/modified tasks provided and add additional tags if requiredon-modify, on-add
CorrectTagNamesChange tag names based on a predefined lookup tableon-modify, on-add
DetectMutuallyExclusiveTagsSee whether the user has specified an incompatible combination of tagson-modify, on-add
PostLatestStartToI3StatusWhen a task is started, send the title of the task to i3status-rs via DBuson-modify
WarnOnTaskCongestionWarn the user if there are too many tasks (due:today)on-exit
92 | 93 | ## Structure of a Hook 94 | 95 | The purpose of this package is to facilitate the development and distribution of 96 | Taskwarrior hooks. To this purpose `install-hooks-shims` allows you to easily 97 | register your own hooks, without having to manually copy items over to the 98 | taskwarrior hooks location. `install-hooks-shims` will install a shim which will 99 | call your hook automatically when required. 100 | 101 | This is an example of a Taskwarrior hook that will be executed on Taskwarrior 102 | exit: 103 | 104 | ```python 105 | from tw_hooks import OnExitHook 106 | class WarnOnTaskCongestion(OnExitHook): 107 | """Warn the user if there are too many tasks.""" 108 | def _on_exit(self, _): # <--- Mandatory to implement this signature 109 | # ... 110 | return 0 111 | ``` 112 | 113 | Assuming that this hook is in a module called `warn_on_task_congestion.py` and 114 | that the directory of this module is in your python path (e.g., by adding it 115 | explicitly to `$PYTHONPATH`), then you can run the following to register your 116 | hook with taskwarrior: 117 | 118 | ```sh 119 | install-hooks-shims -r warn_on_task_congestion 120 | ``` 121 | 122 | During your next Taskwarrior operation, if there are too many due:today tasks, 123 | you should see something like this: 124 | 125 | ```sh 126 | t add +test kalimera 127 | Created task 719. 128 | [WarnOnTaskCongestion] Too many due:today tasks [threshold=9] 129 | ``` 130 | 131 | ## Hooks API 132 | 133 | Subclass one of the following base hooks, and your method is going to be called 134 | during that event: 135 | 136 | - [`OnAddHook`](https://github.com/bergercookie/tw-hooks/blob/master/tw_hooks/base_hooks/on_add_hook.py) 137 | - Implement the `_on_add(self, added_task: TaskT)` method. 138 | - [`OnExitHook`](https://github.com/bergercookie/tw-hooks/blob/master/tw_hooks/base_hooks/on_exit_hook.py) 139 | - Implement the `_on_exit(self, added_modified_tasks: List[TaskT])` method. 140 | - [`OnLaunchHook`](https://github.com/bergercookie/tw-hooks/blob/master/tw_hooks/base_hooks/on_launch_hook.py) 141 | - Implement the `_on_launch(self)` method. 142 | - [`OnModifyHook`](https://github.com/bergercookie/tw-hooks/blob/master/tw_hooks/base_hooks/on_modify_hook.py) 143 | - Implement the `_on_modify(self, original_task: TaskT, modified_task: TaskT)` 144 | method. 145 | 146 | ## Usage instructions for `install-hooks-shims` 147 | 148 | 149 | 150 | ```python 151 | usage: Detect Taskwarrior hooks and register an executable shim for each one of them. 152 | [-h] [-t TASK_DIR] [-a] [-l] 153 | [-r REGISTER_ADDITIONAL [REGISTER_ADDITIONAL ...]] 154 | 155 | optional arguments: 156 | -h, --help show this help message and exit 157 | -t TASK_DIR, --task-dir TASK_DIR 158 | Path to the taskwarrior main directory 159 | -a, --all-hooks Install shims for all the hooks 160 | -l, --list-hooks List the available hooks and exit 161 | -r REGISTER_ADDITIONAL [REGISTER_ADDITIONAL ...], --register-additional REGISTER_ADDITIONAL [REGISTER_ADDITIONAL ...] 162 | 163 | Usage examples: 164 | =============== 165 | 166 | - Install only the WarnOnTaskCongestion hook (assuming you've installed tw_hooks with e.g., pip3) 167 | install-hook-shims -r tw_hooks.hooks.warn_on_task_congestion 168 | 169 | - Install all the available hooks from this repo (assuming you've installed tw_hooks with e.g., pip3) 170 | install-hook-shims --all-hooks 171 | 172 | - Install a custom hook defined in .../dir/mod/hook_name.py. "dir" should be in your PYTHONPATH 173 | install-hook-shims -r mod.hook_name 174 | 175 | - List all the available hooks and exit 176 | install-hook-shims --list-hooks 177 | 178 | ``` 179 | 180 | 181 | 182 | ## Miscellaneous 183 | 184 | - [Contributing Guide](CONTRIBUTING.md) 185 | 186 | ## FAQ 187 | 188 | - Why should I use this over raw taskwarrior hooks? 189 | - Because this package does the heavy lifting pre-processing the input tasks 190 | from the command line. It does so in a robust manner making sure it does 191 | the right thing regardless of weather one or two commands are provided and 192 | being robust to errors (e.g., `utf-8` decoding errors). 193 | - It takes care to make the hooks fail safely even if it can't find required 194 | modules (e.g., if you try invoking `task` from inside a `virtualenv` where 195 | `tw-hooks` is not importable. 196 | - It gives you a class-oriented approach and lets you install multiple hooks 197 | from the same class, thus allowing these hooks to share common 198 | configuration. 199 | - It allows you to keep all your hooks together and keep 200 | them as a package in some other place in your filesystem, e.g., in your 201 | dotfiles and automatically adds the right glue-code so that Taskwarrior your 202 | scripts without having to explicitly place it in `~/.task/hooks` or 203 | symlinking it. 204 | 205 | ## Self Promotion 206 | 207 | If you find this tool useful, please [star it on 208 | Github](https://github.com/bergercookie/tw-hooks) 209 | and consider donating. 210 | 211 | ## Support 212 | 213 | If something doesn't work, feel free to open an issue. You can also find me in 214 | the [#taskwarrior Libera Chat](https://matrix.to/#/#taskwarrior:libera.chat). 215 | 216 | ## TODO List 217 | 218 | See [ISSUES 219 | list](https://github.com/bergercookie/tw-hooks/issues) 220 | for the things that I'm currently either working on or interested in 221 | implementing in the near future. In case there's something you are interesting 222 | in working on, don't hesitate to either ask for clarifications or just do it and 223 | directly make a PR. 224 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "astroid" 3 | version = "2.11.5" 4 | description = "An abstract syntax tree for Python with inference support." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6.2" 8 | 9 | [package.dependencies] 10 | lazy-object-proxy = ">=1.4.0" 11 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} 12 | wrapt = ">=1.11,<2" 13 | 14 | [[package]] 15 | name = "atomicwrites" 16 | version = "1.4.0" 17 | description = "Atomic file writes." 18 | category = "dev" 19 | optional = false 20 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 21 | 22 | [[package]] 23 | name = "attrs" 24 | version = "21.4.0" 25 | description = "Classes Without Boilerplate" 26 | category = "dev" 27 | optional = false 28 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 29 | 30 | [package.extras] 31 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 32 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 33 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 34 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 35 | 36 | [[package]] 37 | name = "black" 38 | version = "22.3.0" 39 | description = "The uncompromising code formatter." 40 | category = "dev" 41 | optional = false 42 | python-versions = ">=3.6.2" 43 | 44 | [package.dependencies] 45 | click = ">=8.0.0" 46 | mypy-extensions = ">=0.4.3" 47 | pathspec = ">=0.9.0" 48 | platformdirs = ">=2" 49 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 50 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 51 | 52 | [package.extras] 53 | colorama = ["colorama (>=0.4.3)"] 54 | d = ["aiohttp (>=3.7.4)"] 55 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 56 | uvloop = ["uvloop (>=0.15.2)"] 57 | 58 | [[package]] 59 | name = "bubop" 60 | version = "0.1.8" 61 | description = "Bergercookie's Useful Bits Of Python" 62 | category = "main" 63 | optional = false 64 | python-versions = ">=3.8,<4.0" 65 | 66 | [package.dependencies] 67 | click = ">=8.0.3,<9.0.0" 68 | loguru = ">=0.5.3,<0.6.0" 69 | python-dateutil = ">=2.8.2,<3.0.0" 70 | PyYAML = ">=5.4.1,<6.0.0" 71 | tqdm = ">=4.62.3,<5.0.0" 72 | 73 | [[package]] 74 | name = "certifi" 75 | version = "2022.5.18.1" 76 | description = "Python package for providing Mozilla's CA Bundle." 77 | category = "dev" 78 | optional = false 79 | python-versions = ">=3.6" 80 | 81 | [[package]] 82 | name = "cfgv" 83 | version = "3.3.1" 84 | description = "Validate configuration and produce human readable error messages." 85 | category = "dev" 86 | optional = false 87 | python-versions = ">=3.6.1" 88 | 89 | [[package]] 90 | name = "charset-normalizer" 91 | version = "2.0.12" 92 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 93 | category = "dev" 94 | optional = false 95 | python-versions = ">=3.5.0" 96 | 97 | [package.extras] 98 | unicode_backport = ["unicodedata2"] 99 | 100 | [[package]] 101 | name = "check-jsonschema" 102 | version = "0.14.3" 103 | description = "A jsonschema CLI and pre-commit hook" 104 | category = "dev" 105 | optional = false 106 | python-versions = ">=3.7" 107 | 108 | [package.dependencies] 109 | identify = ">2.4.10,<3" 110 | importlib-resources = {version = "*", markers = "python_version < \"3.9\""} 111 | jsonschema = ">=4,<5.0" 112 | requests = "<3.0" 113 | "ruamel.yaml" = "0.16.12" 114 | 115 | [package.extras] 116 | dev = ["pytest (<7)", "pytest-cov (<3)", "pytest-xdist (<3)", "responses (==0.18.0)"] 117 | 118 | [[package]] 119 | name = "click" 120 | version = "8.1.3" 121 | description = "Composable command line interface toolkit" 122 | category = "main" 123 | optional = false 124 | python-versions = ">=3.7" 125 | 126 | [package.dependencies] 127 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 128 | 129 | [[package]] 130 | name = "colorama" 131 | version = "0.4.4" 132 | description = "Cross-platform colored terminal text." 133 | category = "main" 134 | optional = false 135 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 136 | 137 | [[package]] 138 | name = "coverage" 139 | version = "6.4.1" 140 | description = "Code coverage measurement for Python" 141 | category = "dev" 142 | optional = false 143 | python-versions = ">=3.7" 144 | 145 | [package.extras] 146 | toml = ["tomli"] 147 | 148 | [[package]] 149 | name = "coveralls" 150 | version = "3.3.1" 151 | description = "Show coverage stats online via coveralls.io" 152 | category = "dev" 153 | optional = false 154 | python-versions = ">= 3.5" 155 | 156 | [package.dependencies] 157 | coverage = ">=4.1,<6.0.0 || >6.1,<6.1.1 || >6.1.1,<7.0" 158 | docopt = ">=0.6.1" 159 | requests = ">=1.0.0" 160 | 161 | [package.extras] 162 | yaml = ["PyYAML (>=3.10)"] 163 | 164 | [[package]] 165 | name = "dill" 166 | version = "0.3.5.1" 167 | description = "serialize all of python" 168 | category = "dev" 169 | optional = false 170 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" 171 | 172 | [package.extras] 173 | graph = ["objgraph (>=1.7.2)"] 174 | 175 | [[package]] 176 | name = "distlib" 177 | version = "0.3.4" 178 | description = "Distribution utilities" 179 | category = "dev" 180 | optional = false 181 | python-versions = "*" 182 | 183 | [[package]] 184 | name = "docopt" 185 | version = "0.6.2" 186 | description = "Pythonic argument parser, that will make you smile" 187 | category = "dev" 188 | optional = false 189 | python-versions = "*" 190 | 191 | [[package]] 192 | name = "fancycompleter" 193 | version = "0.9.1" 194 | description = "colorful TAB completion for Python prompt" 195 | category = "dev" 196 | optional = false 197 | python-versions = "*" 198 | 199 | [package.dependencies] 200 | pyreadline = {version = "*", markers = "platform_system == \"Windows\""} 201 | pyrepl = ">=0.8.2" 202 | 203 | [[package]] 204 | name = "filelock" 205 | version = "3.7.1" 206 | description = "A platform independent file lock." 207 | category = "dev" 208 | optional = false 209 | python-versions = ">=3.7" 210 | 211 | [package.extras] 212 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 213 | testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 214 | 215 | [[package]] 216 | name = "identify" 217 | version = "2.5.1" 218 | description = "File identification library for Python" 219 | category = "dev" 220 | optional = false 221 | python-versions = ">=3.7" 222 | 223 | [package.extras] 224 | license = ["ukkonen"] 225 | 226 | [[package]] 227 | name = "idna" 228 | version = "3.3" 229 | description = "Internationalized Domain Names in Applications (IDNA)" 230 | category = "dev" 231 | optional = false 232 | python-versions = ">=3.5" 233 | 234 | [[package]] 235 | name = "importlib-resources" 236 | version = "5.7.1" 237 | description = "Read resources from Python packages" 238 | category = "dev" 239 | optional = false 240 | python-versions = ">=3.7" 241 | 242 | [package.dependencies] 243 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 244 | 245 | [package.extras] 246 | docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] 247 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] 248 | 249 | [[package]] 250 | name = "iniconfig" 251 | version = "1.1.1" 252 | description = "iniconfig: brain-dead simple config-ini parsing" 253 | category = "dev" 254 | optional = false 255 | python-versions = "*" 256 | 257 | [[package]] 258 | name = "isort" 259 | version = "5.10.1" 260 | description = "A Python utility / library to sort Python imports." 261 | category = "dev" 262 | optional = false 263 | python-versions = ">=3.6.1,<4.0" 264 | 265 | [package.extras] 266 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 267 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 268 | colors = ["colorama (>=0.4.3,<0.5.0)"] 269 | plugins = ["setuptools"] 270 | 271 | [[package]] 272 | name = "jsonschema" 273 | version = "4.6.0" 274 | description = "An implementation of JSON Schema validation for Python" 275 | category = "dev" 276 | optional = false 277 | python-versions = ">=3.7" 278 | 279 | [package.dependencies] 280 | attrs = ">=17.4.0" 281 | importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} 282 | pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" 283 | 284 | [package.extras] 285 | format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] 286 | format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] 287 | 288 | [[package]] 289 | name = "lazy-object-proxy" 290 | version = "1.7.1" 291 | description = "A fast and thorough lazy object proxy." 292 | category = "dev" 293 | optional = false 294 | python-versions = ">=3.6" 295 | 296 | [[package]] 297 | name = "libcst" 298 | version = "0.4.3" 299 | description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." 300 | category = "dev" 301 | optional = false 302 | python-versions = ">=3.7" 303 | 304 | [package.dependencies] 305 | pyyaml = ">=5.2" 306 | typing-extensions = ">=3.7.4.2" 307 | typing-inspect = ">=0.4.0" 308 | 309 | [package.extras] 310 | dev = ["black (==22.3.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.9)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "setuptools-scm (>=6.0.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==1.3)", "usort (==1.0.0rc1)", "setuptools-rust (>=0.12.1)", "slotscheck (>=0.7.1)", "jinja2 (==3.0.3)", "pyre-check (==0.9.9)"] 311 | 312 | [[package]] 313 | name = "loguru" 314 | version = "0.5.3" 315 | description = "Python logging made (stupidly) simple" 316 | category = "main" 317 | optional = false 318 | python-versions = ">=3.5" 319 | 320 | [package.dependencies] 321 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 322 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 323 | 324 | [package.extras] 325 | dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.10b0)", "isort (>=5.1.1)"] 326 | 327 | [[package]] 328 | name = "mccabe" 329 | version = "0.7.0" 330 | description = "McCabe checker, plugin for flake8" 331 | category = "dev" 332 | optional = false 333 | python-versions = ">=3.6" 334 | 335 | [[package]] 336 | name = "mock" 337 | version = "4.0.3" 338 | description = "Rolling backport of unittest.mock for all Pythons" 339 | category = "dev" 340 | optional = false 341 | python-versions = ">=3.6" 342 | 343 | [package.extras] 344 | build = ["twine", "wheel", "blurb"] 345 | docs = ["sphinx"] 346 | test = ["pytest (<5.4)", "pytest-cov"] 347 | 348 | [[package]] 349 | name = "mypy" 350 | version = "0.960" 351 | description = "Optional static typing for Python" 352 | category = "dev" 353 | optional = false 354 | python-versions = ">=3.6" 355 | 356 | [package.dependencies] 357 | mypy-extensions = ">=0.4.3" 358 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 359 | typing-extensions = ">=3.10" 360 | 361 | [package.extras] 362 | dmypy = ["psutil (>=4.0)"] 363 | python2 = ["typed-ast (>=1.4.0,<2)"] 364 | reports = ["lxml"] 365 | 366 | [[package]] 367 | name = "mypy-extensions" 368 | version = "0.4.3" 369 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 370 | category = "dev" 371 | optional = false 372 | python-versions = "*" 373 | 374 | [[package]] 375 | name = "nodeenv" 376 | version = "1.6.0" 377 | description = "Node.js virtual environment builder" 378 | category = "dev" 379 | optional = false 380 | python-versions = "*" 381 | 382 | [[package]] 383 | name = "packaging" 384 | version = "21.3" 385 | description = "Core utilities for Python packages" 386 | category = "dev" 387 | optional = false 388 | python-versions = ">=3.6" 389 | 390 | [package.dependencies] 391 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 392 | 393 | [[package]] 394 | name = "pathspec" 395 | version = "0.9.0" 396 | description = "Utility library for gitignore style pattern matching of file paths." 397 | category = "dev" 398 | optional = false 399 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 400 | 401 | [[package]] 402 | name = "pdbpp" 403 | version = "0.10.3" 404 | description = "pdb++, a drop-in replacement for pdb" 405 | category = "dev" 406 | optional = false 407 | python-versions = "*" 408 | 409 | [package.dependencies] 410 | fancycompleter = ">=0.8" 411 | pygments = "*" 412 | wmctrl = "*" 413 | 414 | [package.extras] 415 | funcsigs = ["funcsigs"] 416 | testing = ["funcsigs", "pytest"] 417 | 418 | [[package]] 419 | name = "platformdirs" 420 | version = "2.5.2" 421 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 422 | category = "dev" 423 | optional = false 424 | python-versions = ">=3.7" 425 | 426 | [package.extras] 427 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 428 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 429 | 430 | [[package]] 431 | name = "pluggy" 432 | version = "1.0.0" 433 | description = "plugin and hook calling mechanisms for python" 434 | category = "dev" 435 | optional = false 436 | python-versions = ">=3.6" 437 | 438 | [package.extras] 439 | dev = ["pre-commit", "tox"] 440 | testing = ["pytest", "pytest-benchmark"] 441 | 442 | [[package]] 443 | name = "pre-commit" 444 | version = "2.19.0" 445 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 446 | category = "dev" 447 | optional = false 448 | python-versions = ">=3.7" 449 | 450 | [package.dependencies] 451 | cfgv = ">=2.0.0" 452 | identify = ">=1.0.0" 453 | nodeenv = ">=0.11.1" 454 | pyyaml = ">=5.1" 455 | toml = "*" 456 | virtualenv = ">=20.0.8" 457 | 458 | [[package]] 459 | name = "py" 460 | version = "1.11.0" 461 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 462 | category = "dev" 463 | optional = false 464 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 465 | 466 | [[package]] 467 | name = "pycln" 468 | version = "1.3.3" 469 | description = "A formatter for finding and removing unused import statements." 470 | category = "dev" 471 | optional = false 472 | python-versions = ">=3.6.2,<4" 473 | 474 | [package.dependencies] 475 | libcst = ">=0.3.10,<0.5.0" 476 | pathspec = ">=0.9.0,<0.10.0" 477 | pyyaml = ">=5.3.1,<7.0.0" 478 | toml = ">=0.10.1,<0.11.0" 479 | typer = ">=0.4.1,<0.5.0" 480 | 481 | [[package]] 482 | name = "pyfakefs" 483 | version = "4.5.6" 484 | description = "pyfakefs implements a fake file system that mocks the Python file system modules." 485 | category = "dev" 486 | optional = false 487 | python-versions = ">=3.6" 488 | 489 | [[package]] 490 | name = "pygments" 491 | version = "2.12.0" 492 | description = "Pygments is a syntax highlighting package written in Python." 493 | category = "dev" 494 | optional = false 495 | python-versions = ">=3.6" 496 | 497 | [[package]] 498 | name = "pylint" 499 | version = "2.14.0" 500 | description = "python code static checker" 501 | category = "dev" 502 | optional = false 503 | python-versions = ">=3.7.2" 504 | 505 | [package.dependencies] 506 | astroid = ">=2.11.5,<=2.12.0-dev0" 507 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 508 | dill = ">=0.2" 509 | isort = ">=4.2.5,<6" 510 | mccabe = ">=0.6,<0.8" 511 | platformdirs = ">=2.2.0" 512 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 513 | tomlkit = ">=0.10.1" 514 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 515 | 516 | [package.extras] 517 | spelling = ["pyenchant (>=3.2,<4.0)"] 518 | testutils = ["gitpython (>3)"] 519 | 520 | [[package]] 521 | name = "pyparsing" 522 | version = "3.0.9" 523 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 524 | category = "dev" 525 | optional = false 526 | python-versions = ">=3.6.8" 527 | 528 | [package.extras] 529 | diagrams = ["railroad-diagrams", "jinja2"] 530 | 531 | [[package]] 532 | name = "pyreadline" 533 | version = "2.1" 534 | description = "A python implmementation of GNU readline." 535 | category = "dev" 536 | optional = false 537 | python-versions = "*" 538 | 539 | [[package]] 540 | name = "pyrepl" 541 | version = "0.9.0" 542 | description = "A library for building flexible command line interfaces" 543 | category = "dev" 544 | optional = false 545 | python-versions = "*" 546 | 547 | [[package]] 548 | name = "pyright" 549 | version = "1.1.252" 550 | description = "Command line wrapper for pyright" 551 | category = "dev" 552 | optional = false 553 | python-versions = ">=3.7" 554 | 555 | [package.dependencies] 556 | nodeenv = ">=1.6.0" 557 | 558 | [package.extras] 559 | all = ["twine (>=3.4.1)"] 560 | dev = ["twine (>=3.4.1)"] 561 | 562 | [[package]] 563 | name = "pyrsistent" 564 | version = "0.18.1" 565 | description = "Persistent/Functional/Immutable data structures" 566 | category = "dev" 567 | optional = false 568 | python-versions = ">=3.7" 569 | 570 | [[package]] 571 | name = "pytest" 572 | version = "7.1.2" 573 | description = "pytest: simple powerful testing with Python" 574 | category = "dev" 575 | optional = false 576 | python-versions = ">=3.7" 577 | 578 | [package.dependencies] 579 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 580 | attrs = ">=19.2.0" 581 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 582 | iniconfig = "*" 583 | packaging = "*" 584 | pluggy = ">=0.12,<2.0" 585 | py = ">=1.8.2" 586 | tomli = ">=1.0.0" 587 | 588 | [package.extras] 589 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 590 | 591 | [[package]] 592 | name = "python-dateutil" 593 | version = "2.8.2" 594 | description = "Extensions to the standard Python datetime module" 595 | category = "main" 596 | optional = false 597 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 598 | 599 | [package.dependencies] 600 | six = ">=1.5" 601 | 602 | [[package]] 603 | name = "pyyaml" 604 | version = "5.4.1" 605 | description = "YAML parser and emitter for Python" 606 | category = "main" 607 | optional = false 608 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 609 | 610 | [[package]] 611 | name = "requests" 612 | version = "2.27.1" 613 | description = "Python HTTP for Humans." 614 | category = "dev" 615 | optional = false 616 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 617 | 618 | [package.dependencies] 619 | certifi = ">=2017.4.17" 620 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 621 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 622 | urllib3 = ">=1.21.1,<1.27" 623 | 624 | [package.extras] 625 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 626 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 627 | 628 | [[package]] 629 | name = "ruamel.yaml" 630 | version = "0.16.12" 631 | description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" 632 | category = "dev" 633 | optional = false 634 | python-versions = "*" 635 | 636 | [package.dependencies] 637 | "ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.9\""} 638 | 639 | [package.extras] 640 | docs = ["ryd"] 641 | jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] 642 | 643 | [[package]] 644 | name = "ruamel.yaml.clib" 645 | version = "0.2.6" 646 | description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" 647 | category = "dev" 648 | optional = false 649 | python-versions = ">=3.5" 650 | 651 | [[package]] 652 | name = "six" 653 | version = "1.16.0" 654 | description = "Python 2 and 3 compatibility utilities" 655 | category = "main" 656 | optional = false 657 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 658 | 659 | [[package]] 660 | name = "toml" 661 | version = "0.10.2" 662 | description = "Python Library for Tom's Obvious, Minimal Language" 663 | category = "dev" 664 | optional = false 665 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 666 | 667 | [[package]] 668 | name = "tomli" 669 | version = "2.0.1" 670 | description = "A lil' TOML parser" 671 | category = "dev" 672 | optional = false 673 | python-versions = ">=3.7" 674 | 675 | [[package]] 676 | name = "tomlkit" 677 | version = "0.11.0" 678 | description = "Style preserving TOML library" 679 | category = "dev" 680 | optional = false 681 | python-versions = ">=3.6,<4.0" 682 | 683 | [[package]] 684 | name = "tqdm" 685 | version = "4.64.0" 686 | description = "Fast, Extensible Progress Meter" 687 | category = "main" 688 | optional = false 689 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 690 | 691 | [package.dependencies] 692 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 693 | 694 | [package.extras] 695 | dev = ["py-make (>=0.1.0)", "twine", "wheel"] 696 | notebook = ["ipywidgets (>=6)"] 697 | slack = ["slack-sdk"] 698 | telegram = ["requests"] 699 | 700 | [[package]] 701 | name = "typer" 702 | version = "0.4.1" 703 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 704 | category = "dev" 705 | optional = false 706 | python-versions = ">=3.6" 707 | 708 | [package.dependencies] 709 | click = ">=7.1.1,<9.0.0" 710 | 711 | [package.extras] 712 | all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] 713 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] 714 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] 715 | test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)"] 716 | 717 | [[package]] 718 | name = "types-python-dateutil" 719 | version = "2.8.17" 720 | description = "Typing stubs for python-dateutil" 721 | category = "dev" 722 | optional = false 723 | python-versions = "*" 724 | 725 | [[package]] 726 | name = "types-pyyaml" 727 | version = "6.0.8" 728 | description = "Typing stubs for PyYAML" 729 | category = "dev" 730 | optional = false 731 | python-versions = "*" 732 | 733 | [[package]] 734 | name = "typing-extensions" 735 | version = "4.2.0" 736 | description = "Backported and Experimental Type Hints for Python 3.7+" 737 | category = "dev" 738 | optional = false 739 | python-versions = ">=3.7" 740 | 741 | [[package]] 742 | name = "typing-inspect" 743 | version = "0.7.1" 744 | description = "Runtime inspection utilities for typing module." 745 | category = "dev" 746 | optional = false 747 | python-versions = "*" 748 | 749 | [package.dependencies] 750 | mypy-extensions = ">=0.3.0" 751 | typing-extensions = ">=3.7.4" 752 | 753 | [[package]] 754 | name = "urllib3" 755 | version = "1.26.9" 756 | description = "HTTP library with thread-safe connection pooling, file post, and more." 757 | category = "dev" 758 | optional = false 759 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 760 | 761 | [package.extras] 762 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 763 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 764 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 765 | 766 | [[package]] 767 | name = "virtualenv" 768 | version = "20.14.1" 769 | description = "Virtual Python Environment builder" 770 | category = "dev" 771 | optional = false 772 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 773 | 774 | [package.dependencies] 775 | distlib = ">=0.3.1,<1" 776 | filelock = ">=3.2,<4" 777 | platformdirs = ">=2,<3" 778 | six = ">=1.9.0,<2" 779 | 780 | [package.extras] 781 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 782 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 783 | 784 | [[package]] 785 | name = "win32-setctime" 786 | version = "1.1.0" 787 | description = "A small Python utility to set file creation time on Windows" 788 | category = "main" 789 | optional = false 790 | python-versions = ">=3.5" 791 | 792 | [package.extras] 793 | dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] 794 | 795 | [[package]] 796 | name = "wmctrl" 797 | version = "0.4" 798 | description = "A tool to programmatically control windows inside X" 799 | category = "dev" 800 | optional = false 801 | python-versions = "*" 802 | 803 | [[package]] 804 | name = "wrapt" 805 | version = "1.14.1" 806 | description = "Module for decorators, wrappers and monkey patching." 807 | category = "dev" 808 | optional = false 809 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 810 | 811 | [[package]] 812 | name = "zipp" 813 | version = "3.8.0" 814 | description = "Backport of pathlib-compatible object wrapper for zip files" 815 | category = "dev" 816 | optional = false 817 | python-versions = ">=3.7" 818 | 819 | [package.extras] 820 | docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] 821 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] 822 | 823 | [metadata] 824 | lock-version = "1.1" 825 | python-versions = "^3.8" 826 | content-hash = "dda0d762940b7c41e7586dcc8c1d9ffcba05a8b824cf24e8dd7811ec994abe23" 827 | 828 | [metadata.files] 829 | astroid = [ 830 | {file = "astroid-2.11.5-py3-none-any.whl", hash = "sha256:14ffbb4f6aa2cf474a0834014005487f7ecd8924996083ab411e7fa0b508ce0b"}, 831 | {file = "astroid-2.11.5.tar.gz", hash = "sha256:f4e4ec5294c4b07ac38bab9ca5ddd3914d4bf46f9006eb5c0ae755755061044e"}, 832 | ] 833 | atomicwrites = [ 834 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 835 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 836 | ] 837 | attrs = [ 838 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 839 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 840 | ] 841 | black = [ 842 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 843 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 844 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 845 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 846 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 847 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 848 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 849 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 850 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 851 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 852 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 853 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 854 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 855 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 856 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 857 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 858 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 859 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 860 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 861 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 862 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 863 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 864 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 865 | ] 866 | bubop = [ 867 | {file = "bubop-0.1.8-py3-none-any.whl", hash = "sha256:1267019023dd9f09cc1390ced2a4c09f7bea200cd71e3e211a1d17450ae8d98b"}, 868 | {file = "bubop-0.1.8.tar.gz", hash = "sha256:36a69f6ffdd3cd2cf606832eebe2d2c5b3dbf743c4207d21be1e3a260ef44891"}, 869 | ] 870 | certifi = [ 871 | {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, 872 | {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, 873 | ] 874 | cfgv = [ 875 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 876 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 877 | ] 878 | charset-normalizer = [ 879 | {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, 880 | {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, 881 | ] 882 | check-jsonschema = [ 883 | {file = "check-jsonschema-0.14.3.tar.gz", hash = "sha256:38bb6f7771fe1b8c521df09b496846934569c5617c7ffae502f2edd2e5a53989"}, 884 | {file = "check_jsonschema-0.14.3-py3-none-any.whl", hash = "sha256:9fd6603266bea1534ea9d672e1568a81672686d15229713a05776e29556a5e26"}, 885 | ] 886 | click = [ 887 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 888 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 889 | ] 890 | colorama = [ 891 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 892 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 893 | ] 894 | coverage = [ 895 | {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, 896 | {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, 897 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, 898 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, 899 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, 900 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, 901 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, 902 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, 903 | {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, 904 | {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, 905 | {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, 906 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, 907 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, 908 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, 909 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, 910 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, 911 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, 912 | {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, 913 | {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, 914 | {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, 915 | {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, 916 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, 917 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, 918 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, 919 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, 920 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, 921 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, 922 | {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, 923 | {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, 924 | {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, 925 | {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, 926 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, 927 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, 928 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, 929 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, 930 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, 931 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, 932 | {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, 933 | {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, 934 | {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, 935 | {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, 936 | ] 937 | coveralls = [ 938 | {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, 939 | {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, 940 | ] 941 | dill = [ 942 | {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, 943 | {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, 944 | ] 945 | distlib = [ 946 | {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, 947 | {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, 948 | ] 949 | docopt = [ 950 | {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, 951 | ] 952 | fancycompleter = [ 953 | {file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"}, 954 | {file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"}, 955 | ] 956 | filelock = [ 957 | {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, 958 | {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, 959 | ] 960 | identify = [ 961 | {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, 962 | {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, 963 | ] 964 | idna = [ 965 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 966 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 967 | ] 968 | importlib-resources = [ 969 | {file = "importlib_resources-5.7.1-py3-none-any.whl", hash = "sha256:e447dc01619b1e951286f3929be820029d48c75eb25d265c28b92a16548212b8"}, 970 | {file = "importlib_resources-5.7.1.tar.gz", hash = "sha256:b6062987dfc51f0fcb809187cffbd60f35df7acb4589091f154214af6d0d49d3"}, 971 | ] 972 | iniconfig = [ 973 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 974 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 975 | ] 976 | isort = [ 977 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 978 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 979 | ] 980 | jsonschema = [ 981 | {file = "jsonschema-4.6.0-py3-none-any.whl", hash = "sha256:1c92d2db1900b668201f1797887d66453ab1fbfea51df8e4b46236689c427baf"}, 982 | {file = "jsonschema-4.6.0.tar.gz", hash = "sha256:9d6397ba4a6c0bf0300736057f649e3e12ecbc07d3e81a0dacb72de4e9801957"}, 983 | ] 984 | lazy-object-proxy = [ 985 | {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, 986 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, 987 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, 988 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, 989 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, 990 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, 991 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, 992 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, 993 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, 994 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, 995 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, 996 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, 997 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, 998 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, 999 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, 1000 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, 1001 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, 1002 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, 1003 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, 1004 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, 1005 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, 1006 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, 1007 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, 1008 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, 1009 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, 1010 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, 1011 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, 1012 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, 1013 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, 1014 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, 1015 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, 1016 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, 1017 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, 1018 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, 1019 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, 1020 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, 1021 | {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, 1022 | ] 1023 | libcst = [ 1024 | {file = "libcst-0.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bea98a8be2b1725784ae01e89519121eba7d81280dcbee40ae03ececd7277cf3"}, 1025 | {file = "libcst-0.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3d9191c764645dddf94d49885e590433fa0ee6d347b07eec86566786e6d2ada5"}, 1026 | {file = "libcst-0.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0f22e9787e44304e7cd9744e543602ab2c1bca8b922cb6237ea08d9a0be3fdd"}, 1027 | {file = "libcst-0.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff147dd77b6ea72e4f2f0abfcd1be11a3108c28cb65e6da666c0b77142033f7c"}, 1028 | {file = "libcst-0.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d744d4a6301c75322f1d88365dccfe402a51e724583a2edc4cba474462cc9419"}, 1029 | {file = "libcst-0.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:ed0f15545eddfdd6270069ce0b2d4c253298817bd676a1a6adddaa1d66c7e28b"}, 1030 | {file = "libcst-0.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6f57056a743853c01bbd21bfd96c2a1b4c317bbc66920f5f2c9999b3dca7233"}, 1031 | {file = "libcst-0.4.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c3d33da8f9b088e118bfc6ecacdd627ac237baeb490f4d7a383af4df4ea4f82"}, 1032 | {file = "libcst-0.4.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df5f51a837fc10cdbf5c61acb467f6c15d5f9ca1d94a84a6a29c4f20ce7b437e"}, 1033 | {file = "libcst-0.4.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f744f60057c8998b856d9baf28765c65574992f4a49830ca350010fc31f4eac4"}, 1034 | {file = "libcst-0.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:88ab371aab82f7241448e263ec42abced649a77cdd21df960268e6df70b3f3f7"}, 1035 | {file = "libcst-0.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:826ea5f10a84625db861ccf35946317f4f29e575261e44c0cd6c24c4dde5c2bb"}, 1036 | {file = "libcst-0.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab5b23796ce66303398bb7b2d27bcb17d2416dacd3d00229c961aed87d79a3b"}, 1037 | {file = "libcst-0.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afc793c95af79e5adc5905713ccddff034d0de3e3da748424b722edf890227de"}, 1038 | {file = "libcst-0.4.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c982387b8e23ad18efbd0287004924931a0b05c91ed5630453faf224bb0b185"}, 1039 | {file = "libcst-0.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc4c25aca45df5f86a6a1c8c219e8c7a90acdaef02b53eb01eafa563381cb0ce"}, 1040 | {file = "libcst-0.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1a395129ecf6c6ce429427f34100ccd99f35898a98187764a4559d9f92166cd0"}, 1041 | {file = "libcst-0.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ca00819affafccb02b2582ec47706712b995c9887cad02bb8efe94a066830f37"}, 1042 | {file = "libcst-0.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:231a9ca446570f9b63d8c2c6dbf6c796fb939a5e4ef9dc0dd9304a21a6c0da16"}, 1043 | {file = "libcst-0.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b08e7a56950479c856183ad6fdf0a21df028d6732e1d19822ec1593e32f700ca"}, 1044 | {file = "libcst-0.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cb70e7e5118234e75d309fcf04931e20f282f16c80dda464fc1b88ef02e52e4"}, 1045 | {file = "libcst-0.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c8c00b24ab39facff463b18b9abc8df7dd063ae0ce9fe2e78e199c9a8572e37"}, 1046 | {file = "libcst-0.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:28f35b9a21b2f8982a8ed3f53b1fdbc5435252409d34d061a3229dc4b413b8c7"}, 1047 | {file = "libcst-0.4.3.tar.gz", hash = "sha256:f79ab61287505d97ed57ead14b78777f48cd6ec5339ca4978987e4c35957a465"}, 1048 | ] 1049 | loguru = [ 1050 | {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, 1051 | {file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"}, 1052 | ] 1053 | mccabe = [ 1054 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 1055 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 1056 | ] 1057 | mock = [ 1058 | {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, 1059 | {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, 1060 | ] 1061 | mypy = [ 1062 | {file = "mypy-0.960-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3a3e525cd76c2c4f90f1449fd034ba21fcca68050ff7c8397bb7dd25dd8b8248"}, 1063 | {file = "mypy-0.960-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7a76dc4f91e92db119b1be293892df8379b08fd31795bb44e0ff84256d34c251"}, 1064 | {file = "mypy-0.960-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffdad80a92c100d1b0fe3d3cf1a4724136029a29afe8566404c0146747114382"}, 1065 | {file = "mypy-0.960-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7d390248ec07fa344b9f365e6ed9d205bd0205e485c555bed37c4235c868e9d5"}, 1066 | {file = "mypy-0.960-cp310-cp310-win_amd64.whl", hash = "sha256:925aa84369a07846b7f3b8556ccade1f371aa554f2bd4fb31cb97a24b73b036e"}, 1067 | {file = "mypy-0.960-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:239d6b2242d6c7f5822163ee082ef7a28ee02e7ac86c35593ef923796826a385"}, 1068 | {file = "mypy-0.960-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f1ba54d440d4feee49d8768ea952137316d454b15301c44403db3f2cb51af024"}, 1069 | {file = "mypy-0.960-cp36-cp36m-win_amd64.whl", hash = "sha256:cb7752b24528c118a7403ee955b6a578bfcf5879d5ee91790667c8ea511d2085"}, 1070 | {file = "mypy-0.960-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:826a2917c275e2ee05b7c7b736c1e6549a35b7ea5a198ca457f8c2ebea2cbecf"}, 1071 | {file = "mypy-0.960-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3eabcbd2525f295da322dff8175258f3fc4c3eb53f6d1929644ef4d99b92e72d"}, 1072 | {file = "mypy-0.960-cp37-cp37m-win_amd64.whl", hash = "sha256:f47322796c412271f5aea48381a528a613f33e0a115452d03ae35d673e6064f8"}, 1073 | {file = "mypy-0.960-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2c7f8bb9619290836a4e167e2ef1f2cf14d70e0bc36c04441e41487456561409"}, 1074 | {file = "mypy-0.960-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fbfb873cf2b8d8c3c513367febde932e061a5f73f762896826ba06391d932b2a"}, 1075 | {file = "mypy-0.960-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc537885891382e08129d9862553b3d00d4be3eb15b8cae9e2466452f52b0117"}, 1076 | {file = "mypy-0.960-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:481f98c6b24383188c928f33dd2f0776690807e12e9989dd0419edd5c74aa53b"}, 1077 | {file = "mypy-0.960-cp38-cp38-win_amd64.whl", hash = "sha256:29dc94d9215c3eb80ac3c2ad29d0c22628accfb060348fd23d73abe3ace6c10d"}, 1078 | {file = "mypy-0.960-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:33d53a232bb79057f33332dbbb6393e68acbcb776d2f571ba4b1d50a2c8ba873"}, 1079 | {file = "mypy-0.960-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d645e9e7f7a5da3ec3bbcc314ebb9bb22c7ce39e70367830eb3c08d0140b9ce"}, 1080 | {file = "mypy-0.960-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:85cf2b14d32b61db24ade8ac9ae7691bdfc572a403e3cb8537da936e74713275"}, 1081 | {file = "mypy-0.960-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a85a20b43fa69efc0b955eba1db435e2ffecb1ca695fe359768e0503b91ea89f"}, 1082 | {file = "mypy-0.960-cp39-cp39-win_amd64.whl", hash = "sha256:0ebfb3f414204b98c06791af37a3a96772203da60636e2897408517fcfeee7a8"}, 1083 | {file = "mypy-0.960-py3-none-any.whl", hash = "sha256:bfd4f6536bd384c27c392a8b8f790fd0ed5c0cf2f63fc2fed7bce56751d53026"}, 1084 | {file = "mypy-0.960.tar.gz", hash = "sha256:d4fccf04c1acf750babd74252e0f2db6bd2ac3aa8fe960797d9f3ef41cf2bfd4"}, 1085 | ] 1086 | mypy-extensions = [ 1087 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 1088 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 1089 | ] 1090 | nodeenv = [ 1091 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 1092 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 1093 | ] 1094 | packaging = [ 1095 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 1096 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 1097 | ] 1098 | pathspec = [ 1099 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 1100 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 1101 | ] 1102 | pdbpp = [ 1103 | {file = "pdbpp-0.10.3-py2.py3-none-any.whl", hash = "sha256:79580568e33eb3d6f6b462b1187f53e10cd8e4538f7d31495c9181e2cf9665d1"}, 1104 | {file = "pdbpp-0.10.3.tar.gz", hash = "sha256:d9e43f4fda388eeb365f2887f4e7b66ac09dce9b6236b76f63616530e2f669f5"}, 1105 | ] 1106 | platformdirs = [ 1107 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 1108 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 1109 | ] 1110 | pluggy = [ 1111 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 1112 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 1113 | ] 1114 | pre-commit = [ 1115 | {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, 1116 | {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, 1117 | ] 1118 | py = [ 1119 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 1120 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 1121 | ] 1122 | pycln = [ 1123 | {file = "pycln-1.3.3-py3-none-any.whl", hash = "sha256:dc59f25ae8147420d641dc9e2338af9850ebf09b55972355332402ec412dc192"}, 1124 | {file = "pycln-1.3.3.tar.gz", hash = "sha256:ce31864993a3b34e021b107847b7975edcf31da05d6ce849562c132e50c2c444"}, 1125 | ] 1126 | pyfakefs = [ 1127 | {file = "pyfakefs-4.5.6-py3-none-any.whl", hash = "sha256:6bb4e27457b0bc90e3ebfe5aed4f1b8c32a18713ba44e925f304bb9b9816a03c"}, 1128 | {file = "pyfakefs-4.5.6.tar.gz", hash = "sha256:914d7bf994406cfbefee0b4d45918f60c15b406afe93f8194a804da5a450a822"}, 1129 | ] 1130 | pygments = [ 1131 | {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, 1132 | {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, 1133 | ] 1134 | pylint = [ 1135 | {file = "pylint-2.14.0-py3-none-any.whl", hash = "sha256:ef64ce5d4c17b8906caeaf2c2914ef3036a3a1b7f4a86f5fbf6caff9067c5f63"}, 1136 | {file = "pylint-2.14.0.tar.gz", hash = "sha256:10d291ea5133645f73fc1b51ca137ad6531223c1461a5632a1db029a9bc033b5"}, 1137 | ] 1138 | pyparsing = [ 1139 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 1140 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 1141 | ] 1142 | pyreadline = [ 1143 | {file = "pyreadline-2.1.win-amd64.exe", hash = "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"}, 1144 | {file = "pyreadline-2.1.win32.exe", hash = "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e"}, 1145 | {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, 1146 | ] 1147 | pyrepl = [ 1148 | {file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"}, 1149 | ] 1150 | pyright = [ 1151 | {file = "pyright-1.1.252-py3-none-any.whl", hash = "sha256:0f436ff34b32a9d67f81ffbe5087812ec84039bdd7ec82b471e54a9291b7eef5"}, 1152 | {file = "pyright-1.1.252.tar.gz", hash = "sha256:df48c20fca2442f2f3016aa94943bc58221e14f071a23befd5248deff25726b5"}, 1153 | ] 1154 | pyrsistent = [ 1155 | {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, 1156 | {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"}, 1157 | {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"}, 1158 | {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"}, 1159 | {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"}, 1160 | {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"}, 1161 | {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"}, 1162 | {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"}, 1163 | {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"}, 1164 | {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"}, 1165 | {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"}, 1166 | {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"}, 1167 | {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"}, 1168 | {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"}, 1169 | {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"}, 1170 | {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"}, 1171 | {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"}, 1172 | {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"}, 1173 | {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, 1174 | {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, 1175 | {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, 1176 | ] 1177 | pytest = [ 1178 | {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, 1179 | {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, 1180 | ] 1181 | python-dateutil = [ 1182 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 1183 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 1184 | ] 1185 | pyyaml = [ 1186 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 1187 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 1188 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 1189 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 1190 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 1191 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 1192 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, 1193 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, 1194 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 1195 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 1196 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 1197 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 1198 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, 1199 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, 1200 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 1201 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 1202 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 1203 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 1204 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, 1205 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, 1206 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 1207 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 1208 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 1209 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 1210 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, 1211 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, 1212 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 1213 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 1214 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 1215 | ] 1216 | requests = [ 1217 | {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, 1218 | {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, 1219 | ] 1220 | "ruamel.yaml" = [ 1221 | {file = "ruamel.yaml-0.16.12-py2.py3-none-any.whl", hash = "sha256:012b9470a0ea06e4e44e99e7920277edf6b46eee0232a04487ea73a7386340a5"}, 1222 | {file = "ruamel.yaml-0.16.12.tar.gz", hash = "sha256:076cc0bc34f1966d920a49f18b52b6ad559fbe656a0748e3535cf7b3f29ebf9e"}, 1223 | ] 1224 | "ruamel.yaml.clib" = [ 1225 | {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, 1226 | {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, 1227 | {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, 1228 | {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, 1229 | {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, 1230 | {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, 1231 | {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, 1232 | {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"}, 1233 | {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"}, 1234 | {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"}, 1235 | {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"}, 1236 | {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"}, 1237 | {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"}, 1238 | {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"}, 1239 | {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"}, 1240 | {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"}, 1241 | {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"}, 1242 | {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"}, 1243 | {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"}, 1244 | {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"}, 1245 | {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"}, 1246 | {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"}, 1247 | {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"}, 1248 | {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, 1249 | {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, 1250 | ] 1251 | six = [ 1252 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1253 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1254 | ] 1255 | toml = [ 1256 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1257 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1258 | ] 1259 | tomli = [ 1260 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1261 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1262 | ] 1263 | tomlkit = [ 1264 | {file = "tomlkit-0.11.0-py3-none-any.whl", hash = "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1"}, 1265 | {file = "tomlkit-0.11.0.tar.gz", hash = "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd"}, 1266 | ] 1267 | tqdm = [ 1268 | {file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"}, 1269 | {file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"}, 1270 | ] 1271 | typer = [ 1272 | {file = "typer-0.4.1-py3-none-any.whl", hash = "sha256:e8467f0ebac0c81366c2168d6ad9f888efdfb6d4e1d3d5b4a004f46fa444b5c3"}, 1273 | {file = "typer-0.4.1.tar.gz", hash = "sha256:5646aef0d936b2c761a10393f0384ee6b5c7fe0bb3e5cd710b17134ca1d99cff"}, 1274 | ] 1275 | types-python-dateutil = [ 1276 | {file = "types-python-dateutil-2.8.17.tar.gz", hash = "sha256:6c54265a221681dd87f61df6743bd5eab060cf1b4086ff65c1a8fd763ed6370e"}, 1277 | {file = "types_python_dateutil-2.8.17-py3-none-any.whl", hash = "sha256:0be7435b4d382d1cd00b8c55a8a90f4e515aaad8a96f8f0bc20c22df046792e5"}, 1278 | ] 1279 | types-pyyaml = [ 1280 | {file = "types-PyYAML-6.0.8.tar.gz", hash = "sha256:d9495d377bb4f9c5387ac278776403eb3b4bb376851025d913eea4c22b4c6438"}, 1281 | {file = "types_PyYAML-6.0.8-py3-none-any.whl", hash = "sha256:56a7b0e8109602785f942a11ebfbd16e97d5d0e79f5fbb077ec4e6a0004837ff"}, 1282 | ] 1283 | typing-extensions = [ 1284 | {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, 1285 | {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, 1286 | ] 1287 | typing-inspect = [ 1288 | {file = "typing_inspect-0.7.1-py2-none-any.whl", hash = "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5"}, 1289 | {file = "typing_inspect-0.7.1-py3-none-any.whl", hash = "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b"}, 1290 | {file = "typing_inspect-0.7.1.tar.gz", hash = "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa"}, 1291 | ] 1292 | urllib3 = [ 1293 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, 1294 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, 1295 | ] 1296 | virtualenv = [ 1297 | {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, 1298 | {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, 1299 | ] 1300 | win32-setctime = [ 1301 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 1302 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 1303 | ] 1304 | wmctrl = [ 1305 | {file = "wmctrl-0.4.tar.gz", hash = "sha256:66cbff72b0ca06a22ec3883ac3a4d7c41078bdae4fb7310f52951769b10e14e0"}, 1306 | ] 1307 | wrapt = [ 1308 | {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, 1309 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, 1310 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, 1311 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, 1312 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, 1313 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, 1314 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, 1315 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, 1316 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, 1317 | {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, 1318 | {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, 1319 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, 1320 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, 1321 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, 1322 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, 1323 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, 1324 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, 1325 | {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, 1326 | {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, 1327 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, 1328 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, 1329 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, 1330 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, 1331 | {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, 1332 | {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, 1333 | {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, 1334 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, 1335 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, 1336 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, 1337 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, 1338 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, 1339 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, 1340 | {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, 1341 | {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, 1342 | {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, 1343 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, 1344 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, 1345 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, 1346 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, 1347 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, 1348 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, 1349 | {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, 1350 | {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, 1351 | {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, 1352 | {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, 1353 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, 1354 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, 1355 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, 1356 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, 1357 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, 1358 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, 1359 | {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, 1360 | {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, 1361 | {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, 1362 | {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, 1363 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, 1364 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, 1365 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, 1366 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, 1367 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, 1368 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, 1369 | {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, 1370 | {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, 1371 | {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, 1372 | ] 1373 | zipp = [ 1374 | {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, 1375 | {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, 1376 | ] 1377 | --------------------------------------------------------------------------------