├── tests ├── __init__.py ├── test_utils.py ├── test_libyear.py └── data │ └── requirements.txt ├── libyear ├── __init__.py ├── libyear ├── utils.py └── pypi.py ├── test_requirements.txt ├── docs └── demo.png ├── .gitignore ├── setup.cfg ├── .github └── workflows │ ├── pythonpublish.yml │ └── pythonpackage.yml ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libyear/__init__.py: -------------------------------------------------------------------------------- 1 | name = "libyear" 2 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=6.0,<7.0 2 | pytest-vcr>=1.0,<1.1 3 | -------------------------------------------------------------------------------- /docs/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirhjafri/libyear/HEAD/docs/demo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | build 4 | dist 5 | libyear.egg-info 6 | .eggs 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Inside of setup.cfg 2 | [metadata] 3 | description-file = README.md 4 | 5 | [aliases] 6 | test=pytest 7 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from libyear.utils import get_requirement_name_and_version, load_requirements 4 | 5 | 6 | def test_loads_from_requirements_file_with_hashes(): 7 | path = Path(__file__).parent / "data" / "requirements.txt" 8 | assert any(line.startswith("appdirs") for line in load_requirements(path)) 9 | 10 | 11 | def test_gets_name_and_version_from_requirements_file_with_hashes(): 12 | path = Path(__file__).parent / "data" / "requirements.txt" 13 | results = { 14 | get_requirement_name_and_version(line) for line in load_requirements(path) 15 | } 16 | 17 | assert ("appdirs", "1.4.3", None) in results 18 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install setuptools wheel twine 20 | - name: Build and publish 21 | env: 22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 24 | run: | 25 | python setup.py sdist bdist_wheel 26 | twine upload dist/* 27 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.6, 3.7, 3.8, 3.9, 3.10-dev] 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | python setup.py install 23 | - name: Lint with flake8 24 | run: | 25 | pip install flake8 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Test with pytest 31 | run: | 32 | pip install -r test_requirements.txt 33 | pytest 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from os import path 3 | 4 | from setuptools import setup 5 | 6 | 7 | this_directory = path.abspath(path.dirname(__file__)) 8 | with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | setup( 12 | name="libyear", 13 | version="0.2.1", 14 | description="A simple measure of software dependency freshness.", 15 | long_description=long_description, 16 | long_description_content_type='text/markdown', 17 | author="nasirhjafri", 18 | url="https://github.com/nasirhjafri/libyear", 19 | classifiers=[ 20 | "Development Status :: 5 - Production/Stable", 21 | "Environment :: Console", 22 | "Environment :: Other Environment", 23 | "Intended Audience :: Developers", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python", 26 | "License :: OSI Approved :: MIT License", 27 | ], 28 | packages=["libyear"], 29 | py_modules=["libyear"], 30 | scripts=["libyear/libyear"], 31 | dependency_links=[], 32 | install_requires=[ 33 | "requests>=2.0.0", 34 | "prettytable>=0.7.2", 35 | "python-dateutil>=2.7.0", 36 | ], 37 | setup_requires=["pytest-runner"], 38 | ) 39 | -------------------------------------------------------------------------------- /libyear/libyear: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | 4 | from prettytable import PrettyTable 5 | 6 | from libyear.pypi import get_lib_days, get_no_of_releases 7 | from libyear.utils import load_requirements, get_requirement_files, get_requirement_name_and_version 8 | 9 | 10 | def main(): 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument('-r', help="Requirements file/path", action='store') 13 | parser.add_argument('--sort', help="Sort by years behind, in descending order", action='store_true') 14 | args = parser.parse_args() 15 | requirements = set() 16 | for req_file in get_requirement_files(args.r): 17 | requirements.update(load_requirements(req_file)) 18 | 19 | pt = PrettyTable() 20 | pt.field_names = ['Library', 'Current Version', 'Latest Version', 'Libyears behind'] 21 | total_days = 0 22 | 23 | for req in requirements: 24 | name, version, version_lt = get_requirement_name_and_version(req) 25 | if not name: 26 | continue 27 | 28 | if not version and not version_lt: 29 | continue 30 | 31 | v, lv, days = get_lib_days(name, version, version_lt) 32 | if v and days > 0: 33 | pt.add_row([name, v, lv, str(round(days / 365, 2))]) 34 | total_days += days 35 | 36 | if args.sort: 37 | pt.sortby = 'Libyears behind' 38 | pt.reversesort = True 39 | 40 | if total_days == 0: 41 | print("Your system is up-to-date!") 42 | else: 43 | print(pt) 44 | print("Your system is %s libyears behind" % str(round(total_days / 365, 2))) 45 | 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /libyear/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | REQUIREMENT_NAME_RE = r'^([^=><]+)' 5 | REQUIREMENT_VERSION_LT_RE = r'<([^$,]*)' 6 | REQUIREMENT_VERSION_LTE_RE = r'[<=]=([^$,]*)' 7 | 8 | 9 | def get_requirement_name_and_version(requirement): 10 | no_requirement = None, None, None 11 | # Remove comments if they are on the same line 12 | requirement = requirement.split()[0].strip() 13 | if not requirement: 14 | return no_requirement 15 | 16 | name = re.findall(REQUIREMENT_NAME_RE, requirement) 17 | if not name: 18 | return no_requirement 19 | 20 | version = re.findall(REQUIREMENT_VERSION_LTE_RE, requirement) 21 | version_lt = re.findall(REQUIREMENT_VERSION_LT_RE, requirement) 22 | if not version_lt and not version: 23 | return no_requirement 24 | 25 | if version: 26 | return name[0], version[0], None 27 | return name[0], None, version_lt[0] 28 | 29 | 30 | def get_requirement_files(path_or_file): 31 | if os.path.isfile(path_or_file): 32 | yield path_or_file 33 | return 34 | for path, subdirs, files in os.walk(path_or_file): 35 | for name in files: 36 | yield os.path.join(path, name) 37 | 38 | 39 | def is_requirement(line): 40 | """ 41 | Return True if the requirement line is a package requirement; 42 | that is, it is not blank, a comment, or editable. 43 | """ 44 | # Remove whitespace at the start/end of the line 45 | line = line.strip() 46 | 47 | # Skip blank lines, comments, and editable installs 48 | return not ( 49 | line == '' or 50 | line.startswith('-r') or 51 | line.startswith('#') or 52 | line.startswith('-e') or 53 | line.startswith('git+') or 54 | line.startswith('--') 55 | ) 56 | 57 | 58 | def load_requirements(*requirements_paths): 59 | """ 60 | Load all requirements from the specified requirements files. 61 | Returns a list of requirement strings. 62 | """ 63 | requirements = set() 64 | for path in requirements_paths: 65 | requirements.update( 66 | line.strip() for line in open(path).readlines() 67 | if is_requirement(line) 68 | ) 69 | return list(requirements) 70 | -------------------------------------------------------------------------------- /libyear/pypi.py: -------------------------------------------------------------------------------- 1 | from distutils.version import LooseVersion 2 | 3 | import dateutil.parser 4 | import requests 5 | 6 | 7 | def get_pypi_data(name, version=None): 8 | """return a dictionary with pypi project data""" 9 | url = "https://pypi.org/pypi/%s/json" % name 10 | if version: 11 | url = "https://pypi.org/pypi/%s/%s/json" % (name, version) 12 | r = requests.get(url) 13 | if r.status_code < 400: 14 | return r.json() 15 | return {} 16 | 17 | 18 | def clean_version(version): 19 | version = [v for v in version if v.isdigit() or v == '.'] 20 | return ''.join(version) 21 | 22 | 23 | def get_version(pypi_data, version, lt=False): 24 | if not version: 25 | return None 26 | 27 | orig_ver = version 28 | releases = pypi_data['releases'] 29 | if version not in releases: 30 | version_data = get_pypi_data(pypi_data['info']['name'], version=version) 31 | version = version_data.get('info', {}).get('version') 32 | if lt: 33 | releases = [(r, rd[-1]['upload_time_iso_8601']) for r, rd in releases.items() if rd] 34 | releases = sorted(releases, key=lambda x: x[1], reverse=True) 35 | releases = [r for r, rd in releases] 36 | if version is None: 37 | curr_ver = LooseVersion(clean_version(orig_ver)) 38 | releases_float = [clean_version(r) for r in releases] 39 | releases_float = [r for r in releases_float if LooseVersion(r) >= curr_ver] 40 | return releases[len(releases_float)] 41 | 42 | idx = releases.index(version) 43 | if idx < len(releases) - 1: 44 | return releases[idx + 1] 45 | return version 46 | 47 | def get_no_of_releases(name, version): 48 | pypi_data = get_pypi_data(name) 49 | if not pypi_data: 50 | return None, None, None, None 51 | 52 | releases = pypi_data['releases'] 53 | 54 | return (len(releases)-list(releases).index(version)) 55 | 56 | def get_version_release_dates(name, version, version_lt): 57 | pypi_data = get_pypi_data(name) 58 | if not pypi_data: 59 | return None, None, None, None 60 | 61 | releases = pypi_data['releases'] 62 | latest_version = pypi_data['info']['version'] 63 | if version_lt: 64 | version = get_version(pypi_data, version_lt, lt=True) 65 | 66 | version = get_version(pypi_data, version) 67 | if version is None: 68 | return None, None, None, None 69 | 70 | try: 71 | latest_version_date = releases[latest_version][-1]['upload_time_iso_8601'] 72 | except IndexError: 73 | print(f'Latest version of {name!r} has no upload time.') 74 | return None, None, None, None 75 | 76 | latest_version_date = dateutil.parser.parse(latest_version_date) 77 | if version not in releases: 78 | return None, latest_version_date, latest_version, latest_version_date 79 | 80 | try: 81 | version_date = releases[version][-1]['upload_time_iso_8601'] 82 | except IndexError: 83 | print(f'Used release of {name}=={version} has no upload time.') 84 | return None, None, None, None 85 | 86 | version_date = dateutil.parser.parse(version_date) 87 | return version, version_date, latest_version, latest_version_date 88 | 89 | 90 | def get_lib_days(name, version, version_lt): 91 | v, cr, lv, lr = get_version_release_dates(name, version, version_lt) 92 | libdays = (lr - cr).days if cr else 0 93 | return v, lv, libdays 94 | -------------------------------------------------------------------------------- /tests/test_libyear.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | from importlib.machinery import SourceFileLoader 5 | from importlib.util import module_from_spec, spec_from_loader 6 | from pathlib import Path 7 | from unittest import mock 8 | 9 | import pytest 10 | 11 | 12 | def load_libyear_module(): 13 | """ As the module has no extension, this workaround is needed to load """ 14 | libyear_path = str(Path(__file__).parent.parent / "libyear/libyear") 15 | spec = spec_from_loader("libyear", SourceFileLoader("libyear", libyear_path)) 16 | libyear = module_from_spec(spec) 17 | spec.loader.exec_module(libyear) 18 | sys.modules['libyear'] = libyear 19 | return libyear 20 | 21 | 22 | libyear = load_libyear_module() 23 | 24 | 25 | @pytest.fixture(scope='module') 26 | def vcr_config(): 27 | return { 28 | 'decode_compressed_response': True 29 | } 30 | 31 | 32 | @pytest.fixture(scope='module') 33 | def vcr_cassette_dir(request): 34 | # Put all cassettes in tests/cassettes/{module}/{test}.yaml 35 | return os.path.join('tests/cassettes/', request.module.__name__) 36 | 37 | 38 | @pytest.mark.vcr() 39 | def test_libyear_main_output(capsys): 40 | requirements_path = str(Path(__file__).parent / 'data' / 'requirements.txt') 41 | 42 | with mock.patch( 43 | 'libyear.argparse.ArgumentParser.parse_args', 44 | return_value=argparse.Namespace(r=requirements_path, sort=False) 45 | ): 46 | libyear.main() 47 | 48 | out, err = capsys.readouterr() 49 | out_lst = out.split("\n") 50 | 51 | assert err == '' 52 | assert out_lst[0:3] == '''\ 53 | +-------------------+-----------------+----------------+-----------------+ 54 | | Library | Current Version | Latest Version | Libyears behind | 55 | +-------------------+-----------------+----------------+-----------------+'''.split("\n") 56 | 57 | ref_lst = '''\ 58 | | pyparsing | 2.4.5 | 2.4.7 | 0.4 | 59 | | pathspec | 0.6.0 | 0.8.1 | 1.1 | 60 | | packaging | 19.2 | 20.8 | 1.23 | 61 | | typed-ast | 1.4.0 | 1.4.1 | 0.61 | 62 | | virtualenv | 16.6.2 | 20.2.2 | 1.4 | 63 | | pre-commit | 1.20.0 | 2.9.3 | 1.11 | 64 | | regex | 2019.12.9 | 2020.11.13 | 0.93 | 65 | | pyyaml | 5.1.1 | 5.3.1 | 0.78 | 66 | | mypy | 0.750 | 0.790 | 0.86 | 67 | | attrs | 19.1.0 | 20.3.0 | 1.68 | 68 | | watchdog | 0.9.0 | 1.0.2 | 2.31 | 69 | | identify | 1.4.5 | 1.5.10 | 1.44 | 70 | | colorama | 0.4.1 | 0.4.4 | 1.89 | 71 | | black | 19.10b0 | 20.8b1 | 0.83 | 72 | | py | 1.8.0 | 1.10.0 | 1.81 | 73 | | more-itertools | 7.0.0 | 8.6.0 | 1.59 | 74 | | pytest-testmon | 0.9.16 | 1.0.3 | 1.39 | 75 | | isort | 4.3.17 | 5.6.4 | 1.52 | 76 | | toml | 0.10.0 | 0.10.2 | 2.08 | 77 | | nodeenv | 1.3.3 | 1.5.0 | 1.8 | 78 | | pytest | 4.4.0 | 6.2.1 | 1.71 | 79 | | tox | 3.14.2 | 3.20.1 | 0.85 | 80 | | pyflakes | 2.1.1 | 2.2.0 | 1.11 | 81 | | atomicwrites | 1.3.0 | 1.4.0 | 1.24 | 82 | | flake8 | 3.7.7 | 3.8.4 | 1.6 | 83 | | coverage | 4.5.3 | 5.3.1 | 1.78 | 84 | | six | 1.12.0 | 1.15.0 | 1.45 | 85 | | flake8-bugbear | 19.3.0 | 20.11.1 | 1.66 | 86 | | click | 7.0 | 7.1.2 | 1.59 | 87 | | mypy-extensions | 0.4.1 | 0.4.3 | 1.15 | 88 | | appdirs | 1.4.3 | 1.4.4 | 3.18 | 89 | | typing-extensions | 3.7.4.1 | 3.7.4.3 | 0.82 | 90 | | cfgv | 2.0.1 | 3.2.0 | 1.03 | 91 | | pycodestyle | 2.5.0 | 2.6.0 | 1.28 |\ 92 | '''.split('\n') 93 | 94 | def table_sort(s): 95 | """remove `|` + any spaces, in order to get alphabetic sort of first column""" 96 | return s.lstrip(" |") 97 | 98 | assert sorted(out_lst[3:-3], key=table_sort) == sorted(ref_lst, key=table_sort) 99 | 100 | assert out_lst[-3:] == '''\ 101 | +-------------------+-----------------+----------------+-----------------+ 102 | Your system is 47.2 libyears behind 103 | '''.split('\n') 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) 2 | [![Open Source Love svg1](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/nasirhjafri/libyear/) 3 | [![PyPI version fury.io](https://badge.fury.io/py/libyear.svg)](https://pypi.python.org/pypi/libyear/) 4 | [![GitHub contributors](https://img.shields.io/github/contributors/nasirhjafri/libyear.svg)](https://GitHub.com/nasirhjafri/libyear/graphs/contributors/) 5 | 6 | 7 | # libyear 8 | 9 | A **simple** measure of software dependency freshness. It is a **single number** telling you how up-to-date your dependencies are. 10 | 11 | https://libyear.com/ 12 | 13 | ![Demo Image](./docs/demo.png) 14 | 15 | ## How to install 16 | `pip install libyear` 17 | 18 | 19 | ## Usage 20 | A single requirement file 21 | `libyear -r requirements.txt` 22 | 23 | A folder with requirement files 24 | `libyear -r requirements/` 25 | 26 | ## Example output 27 | ``` 28 | libyear -r requirements.txt 29 | +-------------------------+-----------------+----------------+-----------------+ 30 | | Library | Current Version | Latest Version | Libyears behind | 31 | +-------------------------+-----------------+----------------+-----------------+ 32 | | pytz | 2015.2 | 2019.3 | 4.54 | 33 | | urllib3 | 1.15.1 | 1.25.7 | 3.58 | 34 | | astroid | 1.5.3 | 2.3.3 | 2.43 | 35 | | django | 1.11.23 | 3.0 | 0.34 | 36 | | django-celery | 3.2.1 | 3.3.1 | 2.54 | 37 | | httpretty | 0.8.3 | 0.9.7 | 5.31 | 38 | | Pygments | 1.6 | 2.5.2 | 6.81 | 39 | | flake8 | 3.6.0 | 3.7.9 | 1.01 | 40 | | django-waffle | 0.14.0 | 0.18.0 | 1.66 | 41 | | requests_oauthlib | 0.8.0 | 1.3.0 | 2.72 | 42 | | django-debug-toolbar | 1.8 | 2.1 | 2.52 | 43 | | libsass | 0.13.3 | 0.19.4 | 2.06 | 44 | | django-storages | 1.6.6 | 1.8 | 1.65 | 45 | | edx-i18n-tools | 0.4.2 | 0.5.0 | 2.02 | 46 | | six | 1.10.0 | 1.13.0 | 4.08 | 47 | | djangorestframework | 3.6.3 | 3.11.0 | 2.58 | 48 | | isort | 4.2.15 | 4.3.21 | 2.05 | 49 | | futures | 2.1.6 | 3.3.0 | 5.5 | 50 | | Pillow | 2.7.0 | 6.2.1 | 4.8 | 51 | | edx-django-release-util | 0.3.1 | 0.3.2 | 2.44 | 52 | | beautifulsoup4 | 4.6.0 | 4.8.1 | 2.42 | 53 | | mysqlclient | 1.4.2.post1 | 1.4.6 | 0.77 | 54 | | newrelic | 4.14.0.115 | 5.4.0.132 | 0.78 | 55 | | redis | 2.10.6 | 3.3.11 | 2.16 | 56 | | oauthlib | 2.1.0 | 3.1.0 | 1.21 | 57 | | django-ses | 0.7.1 | 0.8.13 | 3.65 | 58 | | mock | 1.3.0 | 3.0.5 | 3.79 | 59 | | django-hamlpy | 1.1.1 | 1.2 | 1.52 | 60 | | bottle | 0.12.9 | 0.12.18 | 4.1 | 61 | | pylint-django | 0.7.2 | 2.0.13 | 3.44 | 62 | | user-agents | 1.1.0 | 2.0 | 2.13 | 63 | | jsmin | 2.2.1 | 2.2.2 | 1.15 | 64 | | Markdown | 2.4 | 3.1.1 | 5.26 | 65 | | gunicorn | 0.17.4 | 20.0.4 | 6.59 | 66 | | requests | 2.18.4 | 2.22.0 | 1.75 | 67 | | pylint | 1.7.2 | 2.4.4 | 2.39 | 68 | +-------------------------+-----------------+----------------+-----------------+ 69 | Your system is 103.78 libyears behind 70 | ``` 71 | 72 | ## Example 1 73 | For example, a rails 5.0.0 dependency (released June 30, 2016) is roughly 1 libyear behind the 5.1.2 version (released June 26, 2017). 74 | 75 | ## Simpler is Better 76 | There are obviously more nuanced ways to calculate dependency freshness. The advantage of this approach is its simplicity. You will be able to explain this calculation to your colleagues in about 30s. 77 | 78 | ## Example 2 79 | If your system has two dependencies, the first one year old, the second three, then your system is four libyears out-of-date. 80 | 81 | ## A Healthy App 82 | Apps below 10 libyears are considered to be healthy apps. We regularly rescue projects that are over 100 libyears behind. 83 | 84 | ## Etymology 85 | "lib" is short for "library", the most common form of dependency. 86 | 87 | ## References 88 | J. Cox, E. Bouwers, M. van Eekelen and J. Visser, Measuring Dependency Freshness in Software Systems. In Proceedings of the 37th International Conference on Software Engineering (ICSE 2015), May 2015 https://ericbouwers.github.io/papers/icse15.pdf 89 | -------------------------------------------------------------------------------- /tests/data/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --generate-hashes --output-file=test-requirements.txt test-requirements.in 6 | # 7 | appdirs==1.4.3 \ 8 | --hash=sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92 \ 9 | --hash=sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e \ 10 | # via black 11 | argh==0.26.2 \ 12 | --hash=sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3 \ 13 | --hash=sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65 \ 14 | # via watchdog 15 | aspy.yaml==1.3.0 \ 16 | --hash=sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc \ 17 | --hash=sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45 \ 18 | # via pre-commit 19 | atomicwrites==1.3.0 \ 20 | --hash=sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4 \ 21 | --hash=sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6 \ 22 | # via pytest 23 | attrs==19.1.0 \ 24 | --hash=sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79 \ 25 | --hash=sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399 \ 26 | # via black, flake8-bugbear, pytest 27 | black==19.10b0 \ 28 | --hash=sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b \ 29 | --hash=sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539 30 | cfgv==2.0.1 \ 31 | --hash=sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144 \ 32 | --hash=sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289 \ 33 | # via pre-commit 34 | click==7.0 \ 35 | --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ 36 | --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 \ 37 | # via black 38 | colorama==0.4.1 \ 39 | --hash=sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d \ 40 | --hash=sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48 \ 41 | # via pytest-watch 42 | coverage==4.5.3 \ 43 | --hash=sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9 \ 44 | --hash=sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74 \ 45 | --hash=sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390 \ 46 | --hash=sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8 \ 47 | --hash=sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe \ 48 | --hash=sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf \ 49 | --hash=sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e \ 50 | --hash=sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741 \ 51 | --hash=sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09 \ 52 | --hash=sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd \ 53 | --hash=sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034 \ 54 | --hash=sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420 \ 55 | --hash=sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c \ 56 | --hash=sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab \ 57 | --hash=sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba \ 58 | --hash=sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e \ 59 | --hash=sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609 \ 60 | --hash=sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2 \ 61 | --hash=sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49 \ 62 | --hash=sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b \ 63 | --hash=sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d \ 64 | --hash=sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce \ 65 | --hash=sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9 \ 66 | --hash=sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4 \ 67 | --hash=sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773 \ 68 | --hash=sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723 \ 69 | --hash=sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c \ 70 | --hash=sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f \ 71 | --hash=sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1 \ 72 | --hash=sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260 \ 73 | --hash=sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a 74 | docopt==0.6.2 \ 75 | --hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491 \ 76 | # via pytest-watch 77 | entrypoints==0.3 \ 78 | --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \ 79 | --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 \ 80 | # via flake8 81 | filelock==3.0.12 \ 82 | --hash=sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59 \ 83 | --hash=sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836 \ 84 | # via tox 85 | flake8-bugbear==19.3.0 \ 86 | --hash=sha256:5070774b668be92c4312e5ca82748ddf4ecaa7a24ff062662681bb745c7896eb \ 87 | --hash=sha256:fef9c9826d14ec23187ae1edeb3c6513c4e46bf0e70d86bac38f7d9aabae113d 88 | flake8==3.7.7 \ 89 | --hash=sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661 \ 90 | --hash=sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8 91 | identify==1.4.5 \ 92 | --hash=sha256:0a11379b46d06529795442742a043dc2fa14cd8c995ae81d1febbc5f1c014c87 \ 93 | --hash=sha256:43a5d24ffdb07bc7e21faf68b08e9f526a1f41f0056073f480291539ef961dfd \ 94 | # via pre-commit 95 | isort==4.3.17 \ 96 | --hash=sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43 \ 97 | --hash=sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a 98 | mccabe==0.6.1 \ 99 | --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ 100 | --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f \ 101 | # via flake8 102 | more-itertools==7.0.0 \ 103 | --hash=sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7 \ 104 | --hash=sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a \ 105 | # via pytest 106 | mypy-extensions==0.4.1 \ 107 | --hash=sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812 \ 108 | --hash=sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e \ 109 | # via mypy 110 | mypy==0.750 \ 111 | --hash=sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768 \ 112 | --hash=sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646 \ 113 | --hash=sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c \ 114 | --hash=sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939 \ 115 | --hash=sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279 \ 116 | --hash=sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2 \ 117 | --hash=sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2 \ 118 | --hash=sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640 \ 119 | --hash=sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7 \ 120 | --hash=sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c \ 121 | --hash=sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17 \ 122 | --hash=sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78 \ 123 | --hash=sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931 \ 124 | --hash=sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a 125 | nodeenv==1.3.3 \ 126 | --hash=sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a \ 127 | # via pre-commit 128 | packaging==19.2 \ 129 | --hash=sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47 \ 130 | --hash=sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108 \ 131 | # via tox 132 | pathspec==0.6.0 \ 133 | --hash=sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c \ 134 | # via black 135 | pathtools==0.1.2 \ 136 | --hash=sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0 \ 137 | # via watchdog 138 | pluggy==0.13.1 \ 139 | --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ 140 | --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d \ 141 | # via pytest, tox 142 | pre-commit==1.20.0 \ 143 | --hash=sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e \ 144 | --hash=sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50 145 | py==1.8.0 \ 146 | --hash=sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa \ 147 | --hash=sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53 \ 148 | # via pytest, tox 149 | pycodestyle==2.5.0 \ 150 | --hash=sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56 \ 151 | --hash=sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c \ 152 | # via flake8 153 | pyflakes==2.1.1 \ 154 | --hash=sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0 \ 155 | --hash=sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2 \ 156 | # via flake8 157 | pyparsing==2.4.5 \ 158 | --hash=sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f \ 159 | --hash=sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a \ 160 | # via packaging 161 | pytest-testmon==0.9.16 \ 162 | --hash=sha256:df00594e55f8f8f826e0e345dc23863ebac066eb749f8229c515a0373669c5bb 163 | pytest-watch==4.2.0 \ 164 | --hash=sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9 165 | pytest==4.4.0 \ 166 | --hash=sha256:13c5e9fb5ec5179995e9357111ab089af350d788cbc944c628f3cde72285809b \ 167 | --hash=sha256:f21d2f1fb8200830dcbb5d8ec466a9c9120e20d8b53c7585d180125cce1d297a 168 | pyyaml==5.1.1 \ 169 | --hash=sha256:57acc1d8533cbe51f6662a55434f0dbecfa2b9eaf115bede8f6fd00115a0c0d3 \ 170 | --hash=sha256:588c94b3d16b76cfed8e0be54932e5729cc185caffaa5a451e7ad2f7ed8b4043 \ 171 | --hash=sha256:68c8dd247f29f9a0d09375c9c6b8fdc64b60810ebf07ba4cdd64ceee3a58c7b7 \ 172 | --hash=sha256:70d9818f1c9cd5c48bb87804f2efc8692f1023dac7f1a1a5c61d454043c1d265 \ 173 | --hash=sha256:86a93cccd50f8c125286e637328ff4eef108400dd7089b46a7be3445eecfa391 \ 174 | --hash=sha256:a0f329125a926876f647c9fa0ef32801587a12328b4a3c741270464e3e4fa778 \ 175 | --hash=sha256:a3c252ab0fa1bb0d5a3f6449a4826732f3eb6c0270925548cac342bc9b22c225 \ 176 | --hash=sha256:b4bb4d3f5e232425e25dda21c070ce05168a786ac9eda43768ab7f3ac2770955 \ 177 | --hash=sha256:cd0618c5ba5bda5f4039b9398bb7fb6a317bb8298218c3de25c47c4740e4b95e \ 178 | --hash=sha256:ceacb9e5f8474dcf45b940578591c7f3d960e82f926c707788a570b51ba59190 \ 179 | --hash=sha256:fe6a88094b64132c4bb3b631412e90032e8cfe9745a58370462240b8cb7553cd \ 180 | # via aspy.yaml, pre-commit, watchdog 181 | regex==2019.12.9 \ 182 | --hash=sha256:3dbd8333fd2ebd50977ac8747385a73aa1f546eb6b16fcd83d274470fe11f243 \ 183 | --hash=sha256:40b7d1291a56897927e08bb973f8c186c2feb14c7f708bfe7aaee09483e85a20 \ 184 | --hash=sha256:719978a9145d59fc78509ea1d1bb74243f93583ef2a34dcc5623cf8118ae9726 \ 185 | --hash=sha256:75cf3796f89f75f83207a5c6a6e14eaf57e0369ef0ffff8e22bf36bbcfa0f1de \ 186 | --hash=sha256:77396cf80be8b2a35db863cca4c1a902d88ceeb183adab328b81184e71a5eafe \ 187 | --hash=sha256:77a3799152951d6d14ae5720ca162c97c64f85d4755da585418eac216b736cad \ 188 | --hash=sha256:91235c98283d2bddf1a588f0fbc2da8afa37959294bbd18b76297bdf316ba4d6 \ 189 | --hash=sha256:aaffd68c4c1ed891366d5c390081f4bf6337595e76a157baf453603d8e53fbcb \ 190 | --hash=sha256:ad9e3c7260809c0d1ded100269f78ea0217c0704f1eaaf40a382008461848b45 \ 191 | --hash=sha256:c203c9ee755e9656d0af8fab82754d5a664ebaf707b3f883c7eff6a3dd5151cf \ 192 | --hash=sha256:e865bc508e316a3a09d36c8621596e6599a203bc54f1cd41020a127ccdac468a \ 193 | # via black 194 | rerun==1.0.30 \ 195 | --hash=sha256:33bf86cb3d9dcdb51c6a6712b0cefcdebd7cdffce654d6bc9c8d7aae51e500e9 196 | six==1.12.0 \ 197 | --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ 198 | --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \ 199 | # via cfgv, packaging, pre-commit, pytest, tox 200 | toml==0.10.0 \ 201 | --hash=sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c \ 202 | --hash=sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e \ 203 | # via black, pre-commit, tox 204 | tox==3.14.2 \ 205 | --hash=sha256:7efd010a98339209f3a8292f02909b51c58417bfc6838ab7eca14cf90f96117a \ 206 | --hash=sha256:8dd653bf0c6716a435df363c853cad1f037f9d5fddd0abc90d0f48ad06f39d03 207 | typed-ast==1.4.0 \ 208 | --hash=sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161 \ 209 | --hash=sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e \ 210 | --hash=sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e \ 211 | --hash=sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0 \ 212 | --hash=sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c \ 213 | --hash=sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47 \ 214 | --hash=sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631 \ 215 | --hash=sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4 \ 216 | --hash=sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34 \ 217 | --hash=sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b \ 218 | --hash=sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2 \ 219 | --hash=sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e \ 220 | --hash=sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a \ 221 | --hash=sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233 \ 222 | --hash=sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1 \ 223 | --hash=sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36 \ 224 | --hash=sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d \ 225 | --hash=sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a \ 226 | --hash=sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66 \ 227 | --hash=sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12 \ 228 | # via black, mypy 229 | typeshed==0.0.1 \ 230 | --hash=sha256:097c3f643fb754d38b0538c1d9d2b49f04e68ab6ea53196171c8663d9c473211 \ 231 | --hash=sha256:5a9253eb9e9beaa54ee5aa4e41f1ba1af15ffcd647d0a27e22239b699626d07d 232 | typing-extensions==3.7.4.1 \ 233 | --hash=sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2 \ 234 | --hash=sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d \ 235 | --hash=sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575 \ 236 | # via mypy 237 | virtualenv==16.6.2 \ 238 | --hash=sha256:861bbce3a418110346c70f5c7a696fdcf23a261424e1d28aa4f9362fc2ccbc19 \ 239 | --hash=sha256:ba8ce6a961d842320681fb90a3d564d0e5134f41dacd0e2bae7f02441dde2d52 \ 240 | # via pre-commit, tox 241 | watchdog==0.9.0 \ 242 | --hash=sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d \ 243 | # via pytest-watch 244 | 245 | # WARNING: The following packages were not pinned, but pip requires them to be 246 | # pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. 247 | # setuptools 248 | --------------------------------------------------------------------------------