├── __init__.py ├── subgrab ├── __init__.py ├── utils │ ├── __init__.py │ ├── titleparser.py │ └── directory.py ├── providers │ ├── __init__.py │ ├── subdb.py │ └── subscene.py └── cli.py ├── .flake8 ├── subtitle.bat ├── changelog.rst ├── pyproject.toml ├── .pre-commit-config.yaml ├── .gitignore ├── README.md └── poetry.lock /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /subgrab/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /subgrab/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /subgrab/providers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /subgrab/utils/titleparser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parses name for the media file. 3 | """ 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | max-complexity = 18 4 | select = B,C,E,F,W,T4,B9 5 | ignore = E203, E266, E501, W503, F403, F401 6 | -------------------------------------------------------------------------------- /subtitle.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM (For Windows) Open RUN and type 3 | REM shell:sendto 4 | REM and paste this file there - to download subtitles from the context menu. 5 | cls 6 | IF %1=="" GOTO completed 7 | subgrab -m %~n1 -c 2 8 | :completed 9 | -------------------------------------------------------------------------------- /changelog.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Sub-Grab Changelog 3 | ============== 4 | :Info: Changelog for sub-grab project. 5 | :Author: Rafay Ghafoor 6 | :Copyright: © 2020, Rafay Ghafoor. 7 | :Date: 2020-05-27 8 | :Version: 1.0.4 9 | 10 | .. index:: CHANGELOG 11 | 12 | Version History 13 | =============== 14 | 1.0.4 / 2020-05-27 15 | * Improved subtitles language search context 16 | 17 | 1.0.3 / 2020-04-05 18 | * Added preview 19 | * Added ReadMe 20 | 21 | 1.0.2 / 2020-04-01 22 | * Fixed dependencies 23 | 24 | 1.0.0 / 2020-04-01 25 | * Initial Release 26 | 27 | 0.17 / 2017-11-11 28 | * Only Python 3 Compatible. 29 | 30 | 0.16 / 2017-09-15 31 | * Improve project structure. 32 | * Added support for allSubDB. 33 | * Now prefers allSubDB api over subscene site in downloading subtitles in a directory. 34 | 35 | 0.15 / 2017-09-14 36 | * Improve subtitles selection. 37 | * Implemented logging. 38 | 39 | 0.14 / 2017-09-03 40 | * Fixed search strings. 41 | * Added more precision in media search. 42 | * Removed unused modules and variables. 43 | 44 | 0.13 / 2017-08-31 45 | * Fixed dependencies issue. 46 | 47 | 0.12 / 2017-08-31 48 | * Added number of subtitles to be downloaded for a media. 49 | * Added multiple-languages support. 50 | * Refactored code. 51 | 52 | 0.10 / 2017-08-28 53 | * Initial beta release. 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [[tool.poetry.source]] 2 | name = "subgrab" 3 | url = "https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber" 4 | 5 | [tool.poetry] 6 | name = "subgrab" 7 | version = "1.0.4" 8 | description = "Automated subtitles fetching" 9 | readme = "README.md" 10 | keywords = ["automation", "subtitles", "python", "pyproject.toml", "subscene", "subgrab"] 11 | homepage = "https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber" 12 | repository = "https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber" 13 | documentation = "https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber" 14 | long_description_content_type='text/markdown' 15 | authors = ["Rafay Ghafoor "] 16 | license = "MIT" 17 | 18 | 19 | [tool.poetry.scripts] 20 | subgrab = 'subgrab.cli:main' 21 | 22 | [tool.poetry.dependencies] 23 | python = "^3.6.1" 24 | requests = "^2.23.0" 25 | bs4 = "^0.0.1" 26 | lxml = "^4.5.0" 27 | typing = "^3.7.4" 28 | 29 | [tool.poetry.dev-dependencies] 30 | pytest = "^5.4.1" 31 | black = "^19.10b0" 32 | flake8 = "^3.7.9" 33 | pre-commit = "^2.2.0" 34 | 35 | [tool.black] 36 | line-length = 88 37 | include = '\.pyi?$' 38 | exclude = ''' 39 | /( 40 | \.git 41 | | \.hg 42 | | \.mypy_cache 43 | | \.tox 44 | | \.venv 45 | | _build 46 | | buck-out 47 | | build 48 | | dist 49 | )/ 50 | ''' 51 | [build-system] 52 | requires = ["poetry>=0.12"] 53 | build-backend = "poetry.masonry.api" 54 | -------------------------------------------------------------------------------- /.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: v2.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-merge-conflict 11 | - id: debug-statements 12 | - id: check-added-large-files 13 | - id: flake8 14 | - id: check-ast 15 | - id: check-case-conflict 16 | - id: check-docstring-first 17 | - id: check-builtin-literals 18 | - id: mixed-line-ending 19 | - id: name-tests-test 20 | - repo: https://github.com/miki725/importanize 21 | rev: "0.7" 22 | hooks: 23 | - id: importanize 24 | - repo: https://github.com/psf/black 25 | rev: 19.10b0 26 | hooks: 27 | - id: black 28 | args: [-l, "79"] 29 | - repo: https://gitlab.com/smop/pre-commit-hooks 30 | rev: v1.0.0 31 | hooks: 32 | - id: check-poetry 33 | - repo: https://github.com/pre-commit/mirrors-mypy 34 | rev: "" 35 | hooks: 36 | - id: mypy 37 | args: [--no-strict-optional, --ignore-missing-imports] 38 | additional_dependencies: [tokenize-rt==3.2.0] 39 | - repo: https://github.com/asottile/pyupgrade 40 | rev: v2.1.0 41 | hooks: 42 | - id: pyupgrade 43 | 44 | - repo: https://github.com/asottile/blacken-docs 45 | rev: v1.6.0 46 | hooks: 47 | - id: blacken-docs 48 | args: [-l, "79", '-E'] 49 | # additional_dependencies: [black==...] 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #PYCHARM 2 | 3 | .idea/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | -------------------------------------------------------------------------------- /subgrab/providers/subdb.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import logging 3 | import os.path 4 | 5 | import requests 6 | 7 | 8 | HEADERS = { 9 | "User-agent": "SubDB/1.0 (subgrab/1.0; http://github.com/RafayGhafoor/Subscene-Subtitle-Grabber)" 10 | } 11 | LANGUAGES = ("en", "es", "fr", "it", "nl", "pl", "pt", "ro", "sv", "tr") 12 | DOWNLOAD_URL = "http://api.thesubdb.com/?action=download" 13 | logger = logging.getLogger("subdb.py") 14 | 15 | 16 | def get_hash(name): 17 | readsize = 64 * 1024 18 | with open(name, "rb") as f: 19 | data = f.read(readsize) 20 | f.seek(-readsize, os.SEEK_END) 21 | data += f.read(readsize) 22 | return hashlib.md5(data).hexdigest() 23 | 24 | 25 | def get_sub(file_hash, filename="filename.mkv", language="en"): 26 | logger.info("Downloading subtitles from SubDb") 27 | logger.debug("Language selected for subtitles: %s" % (language)) 28 | if language.lower() in LANGUAGES: 29 | r = requests.get( 30 | DOWNLOAD_URL 31 | + "&hash=" 32 | + file_hash 33 | + "&language=" 34 | + language.lower(), 35 | headers=HEADERS, 36 | ) 37 | logger.debug( 38 | "Status code for {} is {}".format(filename, r.status_code) 39 | ) 40 | if r.status_code == 200: 41 | with open(os.path.splitext(filename)[0] + ".srt", "wb") as f: 42 | for chunk in r.iter_content(chunk_size=150): 43 | if chunk: 44 | f.write(chunk) 45 | logger.info("Downloaded Subtitles for %s" % (filename)) 46 | return 200 47 | elif r.status_code == 404: 48 | logger.info("[SubDB] Subtitle not found for %s" % (filename)) 49 | else: 50 | logger.debug("Invalid file %s" % (filename)) 51 | print("Invalid file") 52 | else: 53 | print("Language not supported") 54 | return 55 | -------------------------------------------------------------------------------- /subgrab/utils/directory.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shutil 4 | from typing import Dict, List 5 | 6 | from subgrab.providers import subdb, subscene 7 | 8 | 9 | logger = logging.getLogger("directory.py") 10 | EXT = [".mp4", ".mkv", ".avi", ".flv"] 11 | MOVIES_DIR: Dict[str, List] = {} # Contains Movies Directories (keys) and the 12 | # files inside them (values = [list]) 13 | REMOVALS = [] # Which already contains subtitles 14 | 15 | 16 | def create_folder(): 17 | """ 18 | Search for video extensions inside the current 19 | directory and If any of the files ending with such 20 | extensions are found (not in folder), create folder 21 | for them and paste the respective file in the corresponding 22 | folder. 23 | """ 24 | for files in [ 25 | i for extension in EXT for i in os.listdir(".") if extension in i 26 | ]: 27 | for extension in EXT: 28 | if files.endswith(extension): 29 | # Creates a folder of same name as file (excluding file extension) 30 | try: 31 | logger.info( 32 | "Moved to folder: {}".format(files.strip(extension)) 33 | ) 34 | os.mkdir(files.strip(extension)) 35 | shutil.move( 36 | files, files.strip(extension) 37 | ) # Moves the file to the new folder 38 | except (OSError, IOError): 39 | logger.debug( 40 | "Cannot create folder for: {}".format( 41 | files.strip(extension) 42 | ) 43 | ) 44 | # If folder exists for the filename or name which 45 | # contains characters out of the ordinal range 46 | 47 | 48 | def get_media_files(): 49 | """ 50 | Obtains media files from the current/specified directory. 51 | """ 52 | for folders, _, files in os.walk("."): 53 | for i in files: 54 | folders = folders.replace("." + os.sep, "") 55 | if i.endswith(".srt"): 56 | REMOVALS.append(folders) 57 | for extension in EXT: 58 | if i.endswith(extension): 59 | if folders not in MOVIES_DIR: 60 | MOVIES_DIR[folders] = [] 61 | MOVIES_DIR[folders].append(i) 62 | # Directories which contains .srt files (Subtitles) 63 | for i in REMOVALS: 64 | if MOVIES_DIR.get( 65 | i 66 | ): # a check for the presence of key which can be already removed or not present 67 | del MOVIES_DIR[i] 68 | 69 | 70 | def dir_dl(sub_count=1): 71 | """ 72 | Download subtitles for the movies in a directory. 73 | """ 74 | # start_time = time.time() 75 | cwd = os.getcwd() 76 | for folders, movies in MOVIES_DIR.items(): 77 | os.chdir(folders) 78 | print("Downloading Subtitles for [{}]".format(folders)) 79 | logger.info("Downloading Subtitles for [{}]".format(folders)) 80 | for mov in movies: 81 | subdb_check = subdb.get_sub( 82 | file_hash=subdb.get_hash(mov), filename=mov, language="en" 83 | ) 84 | if subdb_check == 200: 85 | logger.info("Subtitle Downloaded for {}".format(mov)) 86 | print("Subtitle Downloaded for {}".format(mov)) 87 | 88 | elif subdb != 200: 89 | logger.info( 90 | "Subtitles for [{}] not found on AllSubDB".format(mov) 91 | ) 92 | logger.info("Searching for subtitles on subscene - now") 93 | sub_link = subscene.sel_title(os.path.splitext(mov)[0]) 94 | if sub_link: 95 | # Remove extension from mov argument 96 | selected_sub = subscene.sel_sub( 97 | page=sub_link, name=os.path.splitext(mov)[0] 98 | ) 99 | if selected_sub: 100 | for i in selected_sub: 101 | subscene.dl_sub(i) 102 | else: 103 | print( 104 | "Subtitle not found for [{}]".format( 105 | mov.capitalize() 106 | ) 107 | ) 108 | logger.debug("Subtitle not found for [{}]".format(mov)) 109 | else: 110 | print( 111 | "Subtitle not found for [{}]".format(mov.capitalize()) 112 | ) 113 | logger.debug("Subtitle not found for [{}]".format(mov)) 114 | os.chdir(cwd) 115 | -------------------------------------------------------------------------------- /subgrab/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import logging.config 4 | import os 5 | import sys 6 | 7 | from subgrab.providers import subscene 8 | from subgrab.utils import directory 9 | 10 | 11 | if os.sep == "\\": # Windows OS 12 | log_home = os.path.expanduser( 13 | os.path.join(os.path.join("~", "AppData"), "Local") 14 | ) 15 | else: # Other than Windows 16 | log_home = os.getenv( 17 | "XDG_DATA_HOME", 18 | os.path.expanduser(os.path.join(os.path.join("~", ".local"), "share")), 19 | ) 20 | log_directory = os.path.join(log_home, "Subgrab") 21 | 22 | if not os.path.exists(log_directory): 23 | os.mkdir(log_directory) 24 | 25 | logfile_name = os.path.join(log_directory, "subgrab.log") 26 | DEFAULT_LOGGING = { 27 | "version": 1, 28 | "disable_existing_loggers": False, 29 | "formatters": { 30 | "standard": { 31 | "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s" 32 | }, 33 | }, 34 | "handlers": { 35 | "file": { 36 | "level": "DEBUG", 37 | "class": "logging.handlers.TimedRotatingFileHandler", 38 | "formatter": "standard", 39 | "filename": logfile_name, 40 | }, 41 | }, 42 | "loggers": { 43 | "SubGrab": {"handlers": ["file"], "level": "DEBUG", "propagate": True}, 44 | "directory": { 45 | "handlers": ["file"], 46 | "level": "DEBUG", 47 | "propagate": True, 48 | }, 49 | "subscene": { 50 | "handlers": ["file"], 51 | "level": "DEBUG", 52 | "propagate": True, 53 | }, 54 | }, 55 | } 56 | logging.config.dictConfig(DEFAULT_LOGGING) 57 | logger = logging.getLogger("SubGrab") 58 | 59 | 60 | def main(): 61 | parser = argparse.ArgumentParser() 62 | parser.add_argument( 63 | "-d", "--dir", default=".", help="Specify directory to work in." 64 | ) 65 | parser.add_argument( 66 | "-m", "--media-name", nargs="+", help="Provide movie name." 67 | ) 68 | parser.add_argument( 69 | "-s", "--silent", action="store_true", help="Silent mode." 70 | ) 71 | parser.add_argument( 72 | "-c", 73 | "--count", 74 | type=int, 75 | default=1, 76 | help="Number of subtitles to be downloaded.", 77 | ) 78 | parser.add_argument("-l", "--lang", default="EN", help="Change language.") 79 | args = parser.parse_args() 80 | logger.debug("Input with flags: {}".format(sys.argv)) 81 | logger.info("Initialized SubGrab script") 82 | 83 | if args.silent: 84 | # If mode is silent 85 | logger.debug("Executing Silent Mode") 86 | subscene.MODE = "silent" 87 | 88 | if args.lang: 89 | # Select language - Enter first two letters of the language 90 | if len(args.lang) == 2: 91 | subscene.DEFAULT_LANG = subscene.LANGUAGE[args.lang.upper()] 92 | logger.info("Set Language: {}".format(args.lang)) 93 | else: 94 | sys.exit("Invalid language specified.") 95 | 96 | if args.dir != ".": 97 | # Searches for movies in specified directory. 98 | logger.debug("Running in directory: {}".format(args.dir)) 99 | try: 100 | os.chdir(args.dir) 101 | # Create folder for the files in the current 102 | directory.create_folder() 103 | # directory (which are not in a folder). 104 | directory.get_media_files() 105 | directory.dir_dl() 106 | except Exception as e: 107 | logger.debug("Invalid Directory Input - {}".format(e)) 108 | print("Invalid Directory Input - {}".format(e)) 109 | 110 | elif args.dir == "." and not args.media_name: 111 | # Searches for movies in current directory. 112 | directory.create_folder() 113 | directory.get_media_files() 114 | directory.dir_dl(sub_count=args.count) 115 | 116 | elif args.media_name: 117 | # Searches for the specified movie. 118 | args.media_name = " ".join(args.media_name) 119 | logger.info("Searching For: {}".format(args.media_name)) 120 | sub_link = subscene.sel_title(name=args.media_name.replace(" ", ".")) 121 | logger.info( 122 | "Subtitle Link for {} : {}".format(args.media_name, sub_link) 123 | ) 124 | if sub_link: 125 | for i in subscene.sel_sub( 126 | page=sub_link, sub_count=args.count, name=args.media_name 127 | ): 128 | logger.debug("Downloading Subtitle: {}\n".format(i)) 129 | subscene.dl_sub(i) 130 | 131 | else: 132 | print("Incorrect Arguments Specified.") 133 | 134 | 135 | if __name__ == "__main__": 136 | main() 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SubGrab - Command-line Subtitles Downloader: 2 | 3 | [![Downloads](http://pepy.tech/badge/subgrab)](http://pepy.tech/count/subgrab) 4 | 5 | A utility which provides an ease for automating media i.e., Movies, TV-Series subtitle scraping from multiple providers. 6 | 7 | # Index: 8 | 9 | * [Installation](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#installation) 10 | * [Preview](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#preview) 11 | * [Requirements](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#requirements) 12 | * [Supported Sites](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#providers-supported) 13 | * [Preview](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#preview) 14 | * [Usage](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#usage) 15 | * [Examples](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#examples) 16 | * [Features](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#features) 17 | * [Changelog](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#changelog) 18 | * [Features Upcoming](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber#todo) 19 | 20 | # Status/Version: 21 | 22 | * Current Version: 1.0.4 23 | 24 | # Installation: 25 | 26 | `pip install subgrab` 27 | 28 | # Preview: 29 | 30 | [![asciicast](https://asciinema.org/a/316877.svg)](https://asciinema.org/a/316877/?speed=2) 31 | 32 | # Providers Supported: 33 | 34 | Following sites can be used for subtitle downloading: 35 | 36 |
37 | 38 | | Supported Sites | 39 | | :----------------------------------: | 40 | | SUBSCENE `(-m)` | 41 | | ALLSUBDB `(default for directories)` | 42 | 43 |
44 | 45 | # Usage: 46 | 47 | ``` 48 | Usage: 49 | 50 | subgrab [-h] [-d directory path] [-m Name of the movie/season] [-s Silent Mode] 51 | [-c Number of Subtitles to be downloaded] [-l Custom language] 52 | 53 | Options: 54 | 55 | -h, --help Show this help message and exit. 56 | 57 | -d DIR, --dir DIR Specify directory to work in. 58 | 59 | -m MOVIE_NAME [MOVIE_NAME ...], --movie-name MOVIE_NAME [MOVIE_NAME ...] 60 | Provide Movie Name. 61 | 62 | -s, --silent Silent mode. 63 | 64 | -c COUNT, --count COUNT 65 | Number of subtitles to be downloaded. 66 | 67 | -l LANG, --lang LANG Change language. 68 | ``` 69 | 70 | # Examples: 71 | 72 | ```python 73 | subgrab # To run in current working directory. 74 | 75 | subgrab -m Doctor Strange # For custom movie subtitle download. 76 | 77 | subgrab -m Doctor Strange -s # Silent mode (No prompts i.e., title selection [if not found]). 78 | 79 | subgrab -d "DIRECTORY_PATH" # For specific directory. 80 | 81 | subgrab -m The Intern 2015 -s -l AR # Language specified (First two characters of the language). 82 | 83 | subgrab -m The Intern 2015 -c 3 -s # Download 3 subtitles for the movie. 84 | ``` 85 | 86 | # Changelog: 87 | 88 | * [Changelog](https://github.com/RafayGhafoor/Subscene-Subtitle-Grabber/blob/master/changelog.rst) 89 | 90 | # Note: 91 | 92 | * (For Windows) To use it from the context menu, paste subtitle.bat file in "shell:sendto" (By typing this in RUN). 93 | Taken from Manojmj subtitles script. 94 | 95 | # Features: 96 | 97 | * Two Mode (CLI and Silent inside individual media downloading [-m]) - CLI mode is executed when the title (provided i.e. media name) is not recognized by the site. Mostly when year is not provied (when two or more media names collide). Silent mode is usually executed when year is provided in the argument. Optional, you can also specify silent mode argument - which forces to download subtitles without title selection prompt. The media argument (-m) followed by the silent mode (-s) argument forces silent mode. 98 | 99 | * Subtitles count argument added which allows you to download multiple subtitles for an individual media. This is useful when the exact match is not found and you can download multiple srt files and check them if they are in sync with the media file (integrated in v0.12). 100 | 101 | * Added multiple languages support (v0.12). 102 | 103 | * Allows you to download subtitles for movies by specifying movie name and year (optional). 104 | 105 | * Allows you to download subtitles for media files in a specified directory. 106 | 107 | * Cross-platform (Tested on Linux and Windows). 108 | 109 | * Logs generation on script execution (v0.15) 110 | 111 | * Added Support for the SubDb (v0.16), now first preference for downloading subtitles is SubDB in downloading subtitles from a directory. 112 | 113 | * Initial release (v1.0.0) 114 | 115 | # TODO: 116 | 117 | * [x] Adding support for more languages. 118 | * [x] Adding flags. 119 | * [x] Support for AllSubDB . 120 | * [ ] Support for OpenSubtitles, YifySubtitles. 121 | * [ ] Auto-Sync subtitle naming with the media file when downloaded from subscene. 122 | * [ ] A GUI box which creates a dialogue box (consisting of tick and cross), which waits for the user to check if the subtitle downloaded is synchronized with media file or not - if clicked cross, downloads another subtitle (Process gets repeated unless, correctly synchronized). 123 | * [ ] Watch-folder feature (runs as a service). # Useful for movies automatically downloaded on servers. 124 | * [ ] Argument handling (Replace Argsparse with Click). 125 | * [ ] Using Tabulate for monitoring directory subtitle downloading progress. Three Columns [#, Movie_Folder, Status]. 126 | * [ ] Better Logging. 127 | * [ ] Download subtitles for movies contained in a directory of X year. 128 | * [x] Adding silent mode for downloading subtitles. 129 | * [x] Adding CLI mode for manually downloading subtitles. 130 | * [x] Implement Logging. 131 | * [x] Implementation for seasons episodes. 132 | * [x] Different search algorithms implementation for precise results. 133 | * [x] Improving CLI Mode by displaying the menu according to the site. 134 | * [ ] Multiple subtitle language support also associated with the count variable. 135 | 136 | ``` 137 | For example: 138 | >>> subgrab -m Doctor Strange -s -l AR, EN, SP -c 3 139 | should download 3 subtitles for each language specified 140 | ``` 141 | 142 | * [ ] An option to print list of movies which has subtitles. 143 | * [ ] Creating options in context menu. 144 | * [ ] Display menu which enables to download subtitles for selected directories. (Supporting ranges) 145 | 146 | ``` 147 | For Examples: 148 | (0) Movie 1 149 | (1) Movie 2 150 | . 151 | . 152 | (10) Movie 10 153 | ------------------------------------------------------------------------------------------------------ 154 | (Interactive Prompt) 155 | > 1-3, 6,7,10 156 | 157 | will download subtitles for the directories specified. 158 | ``` 159 | -------------------------------------------------------------------------------- /subgrab/providers/subscene.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import zipfile 5 | 6 | import bs4 7 | import requests 8 | 9 | 10 | logger = logging.getLogger("subscene.py") 11 | SUB_QUERY = "https://subscene.com/subtitles/searchbytitle" 12 | LANGUAGE = { 13 | "AR": "Arabic", 14 | "BU": "Burmese", 15 | "DA": "Danish", 16 | "DU": "Dutch", 17 | "EN": "English", 18 | "FA": "farsi_persian", 19 | "IN": "Indonesian", 20 | "IT": "Italian", 21 | "MA": "Malay", 22 | "SP": "Spanish", 23 | "VI": "Vietnamese", 24 | } 25 | MODE = "prompt" 26 | DEFAULT_LANG = LANGUAGE["EN"] # Default language in which subtitles 27 | # are downloaded. 28 | 29 | 30 | def scrape_page(url, parameter=""): 31 | """ 32 | Retrieve content from a url. 33 | """ 34 | HEADERS = { 35 | "User-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" 36 | } 37 | if parameter: 38 | req = requests.get(url, params={"query": parameter}, headers=HEADERS) 39 | else: 40 | req = requests.get(url, headers=HEADERS) 41 | if req.status_code != 200: 42 | logger.debug("{} not retrieved.".format(req.url)) 43 | return 44 | req_html = bs4.BeautifulSoup(req.content, "lxml") 45 | return req_html 46 | 47 | 48 | def zip_extractor(name): 49 | """ 50 | Extracts zip file obtained from the Subscene site (which contains subtitles). 51 | """ 52 | try: 53 | with zipfile.ZipFile(name, "r") as z: 54 | # srt += [i for i in ZipFile.namelist() if i.endswith('.srt')][0] 55 | z.extractall(".") 56 | os.remove(name) 57 | except Exception as e: 58 | logger.warning("Zip Extractor Error: {}".format(e)) 59 | 60 | 61 | def silent_mode(title_name, category, name=""): 62 | """ 63 | An automatic mode for selecting media title from subscene site. 64 | :param title_name: title names obtained from get_title function. 65 | """ 66 | 67 | def html_navigator(sort_by="Popular"): 68 | """ 69 | Navigates html tree and select title from it. This function is 70 | called twice. For example, the default (Popular) category for 71 | searching in is Popular. It will search title first in popular 72 | category and then in other categories. If default category 73 | changes, this process is reversed. 74 | :param category: selects which category should be searched first in 75 | the html tree. 76 | """ 77 | if ( 78 | sort_by == "Popular" 79 | ): # Searches in Popular Category and the categories next to it. 80 | section = category.find_all_next("div", {"class": "title"}) 81 | else: # Searches in categories above popular tag. 82 | section = title_name.find_all("div", {"class": "title"}) 83 | for results in section: 84 | match = 1 85 | for letter in name.split(): 86 | if letter.lower() in results.a.text.lower(): 87 | # print "NAME: %s, RESULT: %s, MATCH: %s" % (letter, results.a.text, match) 88 | # Loops through the name (list) and if all the elements of the 89 | # list are present in result, returns the link. 90 | if match == len(name.split()): 91 | return ( 92 | "https://subscene.com" 93 | + results.a.get("href") 94 | + "/" 95 | + DEFAULT_LANG 96 | ) 97 | match += 1 98 | 99 | # Searches first in Popular category, if found, returns the title name 100 | obt_link = html_navigator(sort_by="Popular") 101 | if ( 102 | not obt_link 103 | ): # If not found in the popular category, searches in other category 104 | return html_navigator(sort_by="other_than_popular") 105 | return obt_link 106 | 107 | 108 | def cli_mode(titles_name, category): 109 | """ 110 | A manual mode driven by user, allows user to select subtitles manually 111 | from the command-line. 112 | :param titles_name: title names obtained from get_title function. 113 | """ 114 | media_titles = [] # Contains key names of titles_and_links dictionary. 115 | titles_and_links = ( 116 | {} 117 | ) # --> "Doctor Strange" --> "https://subscene.com/.../1345632" 118 | for i, x in enumerate(category.find_all_next("div", {"class": "title"})): 119 | title_text = x.text.encode("ascii", "ignore").decode("utf-8").strip() 120 | titles_and_links[title_text] = x.a.get("href") 121 | print("({}): {}".format(i, title_text)) 122 | media_titles.append(title_text) 123 | 124 | try: 125 | qs = int(input("\nPlease Enter Movie Number: ")) 126 | return ( 127 | "https://subscene.com" 128 | + titles_and_links[media_titles[qs]] 129 | + "/" 130 | + DEFAULT_LANG 131 | ) 132 | 133 | except Exception as e: 134 | logger.warning("Movie Skipped - {}".format(e)) 135 | # If pressed Enter, movie is skipped. 136 | return 137 | 138 | 139 | def sel_title(name): 140 | """ 141 | Select title of the media (i.e., Movie, TV-Series) 142 | :param title_lst: Title Names from the function get_title 143 | :param name: Media Name. For Example: "Doctor Strange" 144 | :param mode: Select CLI Mode or Silent Mode. 145 | URL EXAMPLE: 146 | https://subscene.com/subtitles/title?query=Doctor.Strange 147 | """ 148 | logger.info("Selecting title for name: {}".format(name)) 149 | if not name: 150 | print("Invalid Input.") 151 | return 152 | 153 | soup = scrape_page(url=SUB_QUERY, parameter=name) 154 | logger.info("Searching in query: {}".format(SUB_QUERY + "/?query=" + name)) 155 | try: 156 | if not soup.find("div", {"class": "byTitle"}): 157 | # URL EXAMPLE (RETURNED): 158 | # https://subscene.com/subtitles/searchbytitle?query=pele.birth.of.the.legend 159 | logger.info( 160 | "Searching in release query: {}".format( 161 | SUB_QUERY + "?query=" + name.replace(" ", ".") 162 | ) 163 | ) 164 | return SUB_QUERY + "?query=" + name.replace(" ", ".") 165 | 166 | elif soup.find("div", {"class": "byTitle"}): 167 | # for example, if 'abcedesgg' is search-string 168 | if ( 169 | soup.find("div", {"class": "search-result"}).h2.string 170 | == "No results found" 171 | ): 172 | print( 173 | "Sorry, the subtitles for this media file aren't available." 174 | ) 175 | return 176 | 177 | except Exception as e: 178 | logger.debug("Returning - {}".format(e)) 179 | return 180 | 181 | title_lst = soup.findAll( 182 | "div", {"class": "search-result"} 183 | ) # Creates a list of titles 184 | for titles in title_lst: 185 | popular = titles.find( 186 | "h2", {"class": "popular"} 187 | ) # Searches for the popular tag 188 | if MODE == "prompt": 189 | logger.info("Running in PROMPT mode.") 190 | return cli_mode(titles, category=popular) 191 | else: 192 | logger.info("Running in SILENT mode.") 193 | return silent_mode( 194 | titles, category=popular, name=name.replace(".", " ") 195 | ) 196 | 197 | 198 | # Select Subtitles 199 | def sel_sub(page, sub_count=1, name=""): 200 | """ 201 | Select subtitles from the movie page. 202 | :param sub_count: Number of subtitles to be downloaded. 203 | URL EXAMPLE: 204 | https://subscene.com/subtitles/searchbytitle?query=pele.birth.of.the.legend 205 | """ 206 | # start_time = time.time() 207 | soup = scrape_page(page) 208 | sub_list = [] 209 | current_sub = 0 210 | for link in soup.find_all("td", {"class": "a1"}): 211 | link = link.find("a") 212 | if ( 213 | current_sub < sub_count 214 | and "trailer" not in link.text.lower() 215 | and link.get("href") not in sub_list 216 | and DEFAULT_LANG.lower() in link.get("href") 217 | ): 218 | # if movie = Doctor.Strange.2016, this first condition is not 219 | # going to be executed because the length of the list will be 0 220 | # we format the name by replacing dots with spaces, which will 221 | # split it into the length of the list of two elements (0,1,2) 222 | formatted_name = name.replace(".", " ").split() 223 | if name.lower() in link.text.lower(): 224 | sub_list.append(link.get("href")) 225 | current_sub += 1 226 | 227 | if len(name.split()) > 1: 228 | if ( 229 | name.split()[1].lower() in link.text.lower() 230 | or name.split()[0].lower() in link.text.lower() 231 | ): 232 | sub_list.append(link.get("href")) 233 | current_sub += 1 234 | 235 | elif len(formatted_name) > 1: 236 | if ( 237 | formatted_name[0].lower() in link.text.lower() 238 | or formatted_name[1].lower() in link.text.lower() 239 | ): 240 | sub_list.append(link.get("href")) 241 | current_sub += 1 242 | 243 | # print("--- sel_sub took %s seconds ---" % (time.time() - start_time)) 244 | return ["https://subscene.com" + i for i in sub_list] 245 | 246 | 247 | def dl_sub(page): 248 | """ 249 | Download subtitles obtained from the select_subtitle 250 | function i.e., movie subtitles links. 251 | """ 252 | # start_time = time.time() 253 | soup = scrape_page(page) 254 | div = soup.find("div", {"class": "download"}) 255 | down_link = "https://subscene.com" + div.find("a").get("href") 256 | r = requests.get(down_link, stream=True) 257 | for found_sub in re.findall( 258 | "filename=(.+)", r.headers["content-disposition"] 259 | ): 260 | with open(found_sub.replace("-", " "), "wb") as f: 261 | for chunk in r.iter_content(chunk_size=150): 262 | if chunk: 263 | f.write(chunk) 264 | zip_extractor(found_sub.replace("-", " ")) 265 | print( 266 | "Subtitle ({}) - Downloaded\n".format( 267 | found_sub.replace("-", " ").capitalize() 268 | ) 269 | ) 270 | # print("--- download_sub took %s seconds ---" % (time.time() - start_time)) 271 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 4 | name = "appdirs" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.4.3" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "Atomic file writes." 12 | marker = "sys_platform == \"win32\"" 13 | name = "atomicwrites" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | version = "1.3.0" 17 | 18 | [[package]] 19 | category = "dev" 20 | description = "Classes Without Boilerplate" 21 | name = "attrs" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 24 | version = "19.3.0" 25 | 26 | [package.extras] 27 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 28 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 29 | docs = ["sphinx", "zope.interface"] 30 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 31 | 32 | [[package]] 33 | category = "main" 34 | description = "Screen-scraping library" 35 | name = "beautifulsoup4" 36 | optional = false 37 | python-versions = "*" 38 | version = "4.8.2" 39 | 40 | [package.dependencies] 41 | soupsieve = ">=1.2" 42 | 43 | [package.extras] 44 | html5lib = ["html5lib"] 45 | lxml = ["lxml"] 46 | 47 | [[package]] 48 | category = "dev" 49 | description = "The uncompromising code formatter." 50 | name = "black" 51 | optional = false 52 | python-versions = ">=3.6" 53 | version = "19.10b0" 54 | 55 | [package.dependencies] 56 | appdirs = "*" 57 | attrs = ">=18.1.0" 58 | click = ">=6.5" 59 | pathspec = ">=0.6,<1" 60 | regex = "*" 61 | toml = ">=0.9.4" 62 | typed-ast = ">=1.4.0" 63 | 64 | [package.extras] 65 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 66 | 67 | [[package]] 68 | category = "main" 69 | description = "Dummy package for Beautiful Soup" 70 | name = "bs4" 71 | optional = false 72 | python-versions = "*" 73 | version = "0.0.1" 74 | 75 | [package.dependencies] 76 | beautifulsoup4 = "*" 77 | 78 | [[package]] 79 | category = "main" 80 | description = "Python package for providing Mozilla's CA Bundle." 81 | name = "certifi" 82 | optional = false 83 | python-versions = "*" 84 | version = "2019.11.28" 85 | 86 | [[package]] 87 | category = "dev" 88 | description = "Validate configuration and produce human readable error messages." 89 | name = "cfgv" 90 | optional = false 91 | python-versions = ">=3.6.1" 92 | version = "3.1.0" 93 | 94 | [[package]] 95 | category = "main" 96 | description = "Universal encoding detector for Python 2 and 3" 97 | name = "chardet" 98 | optional = false 99 | python-versions = "*" 100 | version = "3.0.4" 101 | 102 | [[package]] 103 | category = "dev" 104 | description = "Composable command line interface toolkit" 105 | name = "click" 106 | optional = false 107 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 108 | version = "7.1.1" 109 | 110 | [[package]] 111 | category = "dev" 112 | description = "Cross-platform colored terminal text." 113 | marker = "sys_platform == \"win32\"" 114 | name = "colorama" 115 | optional = false 116 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 117 | version = "0.4.3" 118 | 119 | [[package]] 120 | category = "dev" 121 | description = "Distribution utilities" 122 | name = "distlib" 123 | optional = false 124 | python-versions = "*" 125 | version = "0.3.0" 126 | 127 | [[package]] 128 | category = "dev" 129 | description = "Discover and load entry points from installed packages." 130 | name = "entrypoints" 131 | optional = false 132 | python-versions = ">=2.7" 133 | version = "0.3" 134 | 135 | [[package]] 136 | category = "dev" 137 | description = "A platform independent file lock." 138 | name = "filelock" 139 | optional = false 140 | python-versions = "*" 141 | version = "3.0.12" 142 | 143 | [[package]] 144 | category = "dev" 145 | description = "the modular source code checker: pep8, pyflakes and co" 146 | name = "flake8" 147 | optional = false 148 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 149 | version = "3.7.9" 150 | 151 | [package.dependencies] 152 | entrypoints = ">=0.3.0,<0.4.0" 153 | mccabe = ">=0.6.0,<0.7.0" 154 | pycodestyle = ">=2.5.0,<2.6.0" 155 | pyflakes = ">=2.1.0,<2.2.0" 156 | 157 | [[package]] 158 | category = "dev" 159 | description = "File identification library for Python" 160 | name = "identify" 161 | optional = false 162 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 163 | version = "1.4.13" 164 | 165 | [package.extras] 166 | license = ["editdistance"] 167 | 168 | [[package]] 169 | category = "main" 170 | description = "Internationalized Domain Names in Applications (IDNA)" 171 | name = "idna" 172 | optional = false 173 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 174 | version = "2.9" 175 | 176 | [[package]] 177 | category = "dev" 178 | description = "Read metadata from Python packages" 179 | marker = "python_version < \"3.8\"" 180 | name = "importlib-metadata" 181 | optional = false 182 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 183 | version = "1.6.0" 184 | 185 | [package.dependencies] 186 | zipp = ">=0.5" 187 | 188 | [package.extras] 189 | docs = ["sphinx", "rst.linker"] 190 | testing = ["packaging", "importlib-resources"] 191 | 192 | [[package]] 193 | category = "dev" 194 | description = "Read resources from Python packages" 195 | marker = "python_version < \"3.7\"" 196 | name = "importlib-resources" 197 | optional = false 198 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 199 | version = "1.4.0" 200 | 201 | [package.dependencies] 202 | [package.dependencies.importlib-metadata] 203 | python = "<3.8" 204 | version = "*" 205 | 206 | [package.dependencies.zipp] 207 | python = "<3.8" 208 | version = ">=0.4" 209 | 210 | [package.extras] 211 | docs = ["sphinx", "rst.linker", "jaraco.packaging"] 212 | 213 | [[package]] 214 | category = "main" 215 | description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." 216 | name = "lxml" 217 | optional = false 218 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" 219 | version = "4.5.0" 220 | 221 | [package.extras] 222 | cssselect = ["cssselect (>=0.7)"] 223 | html5 = ["html5lib"] 224 | htmlsoup = ["beautifulsoup4"] 225 | source = ["Cython (>=0.29.7)"] 226 | 227 | [[package]] 228 | category = "dev" 229 | description = "McCabe checker, plugin for flake8" 230 | name = "mccabe" 231 | optional = false 232 | python-versions = "*" 233 | version = "0.6.1" 234 | 235 | [[package]] 236 | category = "dev" 237 | description = "More routines for operating on iterables, beyond itertools" 238 | name = "more-itertools" 239 | optional = false 240 | python-versions = ">=3.5" 241 | version = "8.2.0" 242 | 243 | [[package]] 244 | category = "dev" 245 | description = "Node.js virtual environment builder" 246 | name = "nodeenv" 247 | optional = false 248 | python-versions = "*" 249 | version = "1.3.5" 250 | 251 | [[package]] 252 | category = "dev" 253 | description = "Core utilities for Python packages" 254 | name = "packaging" 255 | optional = false 256 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 257 | version = "20.3" 258 | 259 | [package.dependencies] 260 | pyparsing = ">=2.0.2" 261 | six = "*" 262 | 263 | [[package]] 264 | category = "dev" 265 | description = "Utility library for gitignore style pattern matching of file paths." 266 | name = "pathspec" 267 | optional = false 268 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 269 | version = "0.7.0" 270 | 271 | [[package]] 272 | category = "dev" 273 | description = "plugin and hook calling mechanisms for python" 274 | name = "pluggy" 275 | optional = false 276 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 277 | version = "0.13.1" 278 | 279 | [package.dependencies] 280 | [package.dependencies.importlib-metadata] 281 | python = "<3.8" 282 | version = ">=0.12" 283 | 284 | [package.extras] 285 | dev = ["pre-commit", "tox"] 286 | 287 | [[package]] 288 | category = "dev" 289 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 290 | name = "pre-commit" 291 | optional = false 292 | python-versions = ">=3.6.1" 293 | version = "2.2.0" 294 | 295 | [package.dependencies] 296 | cfgv = ">=2.0.0" 297 | identify = ">=1.0.0" 298 | nodeenv = ">=0.11.1" 299 | pyyaml = ">=5.1" 300 | toml = "*" 301 | virtualenv = ">=15.2" 302 | 303 | [package.dependencies.importlib-metadata] 304 | python = "<3.8" 305 | version = "*" 306 | 307 | [package.dependencies.importlib-resources] 308 | python = "<3.7" 309 | version = "*" 310 | 311 | [[package]] 312 | category = "dev" 313 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 314 | name = "py" 315 | optional = false 316 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 317 | version = "1.8.1" 318 | 319 | [[package]] 320 | category = "dev" 321 | description = "Python style guide checker" 322 | name = "pycodestyle" 323 | optional = false 324 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 325 | version = "2.5.0" 326 | 327 | [[package]] 328 | category = "dev" 329 | description = "passive checker of Python programs" 330 | name = "pyflakes" 331 | optional = false 332 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 333 | version = "2.1.1" 334 | 335 | [[package]] 336 | category = "dev" 337 | description = "Python parsing module" 338 | name = "pyparsing" 339 | optional = false 340 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 341 | version = "2.4.6" 342 | 343 | [[package]] 344 | category = "dev" 345 | description = "pytest: simple powerful testing with Python" 346 | name = "pytest" 347 | optional = false 348 | python-versions = ">=3.5" 349 | version = "5.4.1" 350 | 351 | [package.dependencies] 352 | atomicwrites = ">=1.0" 353 | attrs = ">=17.4.0" 354 | colorama = "*" 355 | more-itertools = ">=4.0.0" 356 | packaging = "*" 357 | pluggy = ">=0.12,<1.0" 358 | py = ">=1.5.0" 359 | wcwidth = "*" 360 | 361 | [package.dependencies.importlib-metadata] 362 | python = "<3.8" 363 | version = ">=0.12" 364 | 365 | [package.extras] 366 | checkqa-mypy = ["mypy (v0.761)"] 367 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 368 | 369 | [[package]] 370 | category = "dev" 371 | description = "YAML parser and emitter for Python" 372 | name = "pyyaml" 373 | optional = false 374 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 375 | version = "5.3.1" 376 | 377 | [[package]] 378 | category = "dev" 379 | description = "Alternative regular expression module, to replace re." 380 | name = "regex" 381 | optional = false 382 | python-versions = "*" 383 | version = "2020.2.20" 384 | 385 | [[package]] 386 | category = "main" 387 | description = "Python HTTP for Humans." 388 | name = "requests" 389 | optional = false 390 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 391 | version = "2.23.0" 392 | 393 | [package.dependencies] 394 | certifi = ">=2017.4.17" 395 | chardet = ">=3.0.2,<4" 396 | idna = ">=2.5,<3" 397 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 398 | 399 | [package.extras] 400 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 401 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] 402 | 403 | [[package]] 404 | category = "dev" 405 | description = "Python 2 and 3 compatibility utilities" 406 | name = "six" 407 | optional = false 408 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 409 | version = "1.14.0" 410 | 411 | [[package]] 412 | category = "main" 413 | description = "A modern CSS selector implementation for Beautiful Soup." 414 | name = "soupsieve" 415 | optional = false 416 | python-versions = ">=3.5" 417 | version = "2.0" 418 | 419 | [[package]] 420 | category = "dev" 421 | description = "Python Library for Tom's Obvious, Minimal Language" 422 | name = "toml" 423 | optional = false 424 | python-versions = "*" 425 | version = "0.10.0" 426 | 427 | [[package]] 428 | category = "dev" 429 | description = "a fork of Python 2 and 3 ast modules with type comment support" 430 | name = "typed-ast" 431 | optional = false 432 | python-versions = "*" 433 | version = "1.4.1" 434 | 435 | [[package]] 436 | category = "main" 437 | description = "Type Hints for Python" 438 | name = "typing" 439 | optional = false 440 | python-versions = "*" 441 | version = "3.7.4.1" 442 | 443 | [[package]] 444 | category = "main" 445 | description = "HTTP library with thread-safe connection pooling, file post, and more." 446 | name = "urllib3" 447 | optional = false 448 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 449 | version = "1.25.8" 450 | 451 | [package.extras] 452 | brotli = ["brotlipy (>=0.6.0)"] 453 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 454 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 455 | 456 | [[package]] 457 | category = "dev" 458 | description = "Virtual Python Environment builder" 459 | name = "virtualenv" 460 | optional = false 461 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 462 | version = "20.0.15" 463 | 464 | [package.dependencies] 465 | appdirs = ">=1.4.3,<2" 466 | distlib = ">=0.3.0,<1" 467 | filelock = ">=3.0.0,<4" 468 | six = ">=1.9.0,<2" 469 | 470 | [package.dependencies.importlib-metadata] 471 | python = "<3.8" 472 | version = ">=0.12,<2" 473 | 474 | [package.dependencies.importlib-resources] 475 | python = "<3.7" 476 | version = ">=1.0,<2" 477 | 478 | [package.extras] 479 | docs = ["sphinx (>=2.0.0,<3)", "sphinx-argparse (>=0.2.5,<1)", "sphinx-rtd-theme (>=0.4.3,<1)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2,<1)"] 480 | testing = ["pytest (>=4.0.0,<6)", "coverage (>=4.5.1,<6)", "pytest-mock (>=2.0.0,<3)", "pytest-env (>=0.6.2,<1)", "pytest-timeout (>=1.3.4,<2)", "packaging (>=20.0)", "xonsh (>=0.9.13,<1)"] 481 | 482 | [[package]] 483 | category = "dev" 484 | description = "Measures number of Terminal column cells of wide-character codes" 485 | name = "wcwidth" 486 | optional = false 487 | python-versions = "*" 488 | version = "0.1.9" 489 | 490 | [[package]] 491 | category = "dev" 492 | description = "Backport of pathlib-compatible object wrapper for zip files" 493 | marker = "python_version < \"3.8\"" 494 | name = "zipp" 495 | optional = false 496 | python-versions = ">=3.6" 497 | version = "3.1.0" 498 | 499 | [package.extras] 500 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 501 | testing = ["jaraco.itertools", "func-timeout"] 502 | 503 | [metadata] 504 | content-hash = "140d6435fa9355764a0f0a1b9c7fdb2b69bb5480151235ba278e6950b39a7da0" 505 | python-versions = "^3.6.1" 506 | 507 | [metadata.files] 508 | appdirs = [ 509 | {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, 510 | {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, 511 | ] 512 | atomicwrites = [ 513 | {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, 514 | {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, 515 | ] 516 | attrs = [ 517 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 518 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 519 | ] 520 | beautifulsoup4 = [ 521 | {file = "beautifulsoup4-4.8.2-py2-none-any.whl", hash = "sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae"}, 522 | {file = "beautifulsoup4-4.8.2-py3-none-any.whl", hash = "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887"}, 523 | {file = "beautifulsoup4-4.8.2.tar.gz", hash = "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a"}, 524 | ] 525 | black = [ 526 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 527 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 528 | ] 529 | bs4 = [ 530 | {file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"}, 531 | ] 532 | certifi = [ 533 | {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, 534 | {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, 535 | ] 536 | cfgv = [ 537 | {file = "cfgv-3.1.0-py2.py3-none-any.whl", hash = "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53"}, 538 | {file = "cfgv-3.1.0.tar.gz", hash = "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513"}, 539 | ] 540 | chardet = [ 541 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 542 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 543 | ] 544 | click = [ 545 | {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, 546 | {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, 547 | ] 548 | colorama = [ 549 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 550 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 551 | ] 552 | distlib = [ 553 | {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, 554 | ] 555 | entrypoints = [ 556 | {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, 557 | {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, 558 | ] 559 | filelock = [ 560 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 561 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 562 | ] 563 | flake8 = [ 564 | {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, 565 | {file = "flake8-3.7.9.tar.gz", hash = "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb"}, 566 | ] 567 | identify = [ 568 | {file = "identify-1.4.13-py2.py3-none-any.whl", hash = "sha256:a7577a1f55cee1d21953a5cf11a3c839ab87f5ef909a4cba6cf52ed72b4c6059"}, 569 | {file = "identify-1.4.13.tar.gz", hash = "sha256:ab246293e6585a1c6361a505b68d5b501a0409310932b7de2c2ead667b564d89"}, 570 | ] 571 | idna = [ 572 | {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, 573 | {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, 574 | ] 575 | importlib-metadata = [ 576 | {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, 577 | {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, 578 | ] 579 | importlib-resources = [ 580 | {file = "importlib_resources-1.4.0-py2.py3-none-any.whl", hash = "sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8"}, 581 | {file = "importlib_resources-1.4.0.tar.gz", hash = "sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2"}, 582 | ] 583 | lxml = [ 584 | {file = "lxml-4.5.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c"}, 585 | {file = "lxml-4.5.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd"}, 586 | {file = "lxml-4.5.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261"}, 587 | {file = "lxml-4.5.0-cp27-cp27m-win32.whl", hash = "sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89"}, 588 | {file = "lxml-4.5.0-cp27-cp27m-win_amd64.whl", hash = "sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a"}, 589 | {file = "lxml-4.5.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128"}, 590 | {file = "lxml-4.5.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"}, 591 | {file = "lxml-4.5.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb"}, 592 | {file = "lxml-4.5.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8"}, 593 | {file = "lxml-4.5.0-cp35-cp35m-win32.whl", hash = "sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77"}, 594 | {file = "lxml-4.5.0-cp35-cp35m-win_amd64.whl", hash = "sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081"}, 595 | {file = "lxml-4.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9"}, 596 | {file = "lxml-4.5.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717"}, 597 | {file = "lxml-4.5.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15"}, 598 | {file = "lxml-4.5.0-cp36-cp36m-win32.whl", hash = "sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7"}, 599 | {file = "lxml-4.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012"}, 600 | {file = "lxml-4.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6"}, 601 | {file = "lxml-4.5.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679"}, 602 | {file = "lxml-4.5.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc"}, 603 | {file = "lxml-4.5.0-cp37-cp37m-win32.whl", hash = "sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a"}, 604 | {file = "lxml-4.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8"}, 605 | {file = "lxml-4.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72"}, 606 | {file = "lxml-4.5.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1"}, 607 | {file = "lxml-4.5.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a"}, 608 | {file = "lxml-4.5.0-cp38-cp38-win32.whl", hash = "sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f"}, 609 | {file = "lxml-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3"}, 610 | {file = "lxml-4.5.0.tar.gz", hash = "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60"}, 611 | ] 612 | mccabe = [ 613 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 614 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 615 | ] 616 | more-itertools = [ 617 | {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, 618 | {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, 619 | ] 620 | nodeenv = [ 621 | {file = "nodeenv-1.3.5-py2.py3-none-any.whl", hash = "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212"}, 622 | ] 623 | packaging = [ 624 | {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, 625 | {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, 626 | ] 627 | pathspec = [ 628 | {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, 629 | {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"}, 630 | ] 631 | pluggy = [ 632 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 633 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 634 | ] 635 | pre-commit = [ 636 | {file = "pre_commit-2.2.0-py2.py3-none-any.whl", hash = "sha256:487c675916e6f99d355ec5595ad77b325689d423ef4839db1ed2f02f639c9522"}, 637 | {file = "pre_commit-2.2.0.tar.gz", hash = "sha256:c0aa11bce04a7b46c5544723aedf4e81a4d5f64ad1205a30a9ea12d5e81969e1"}, 638 | ] 639 | py = [ 640 | {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, 641 | {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, 642 | ] 643 | pycodestyle = [ 644 | {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, 645 | {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, 646 | ] 647 | pyflakes = [ 648 | {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, 649 | {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, 650 | ] 651 | pyparsing = [ 652 | {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"}, 653 | {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, 654 | ] 655 | pytest = [ 656 | {file = "pytest-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"}, 657 | {file = "pytest-5.4.1.tar.gz", hash = "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"}, 658 | ] 659 | pyyaml = [ 660 | {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, 661 | {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, 662 | {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, 663 | {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, 664 | {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, 665 | {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, 666 | {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, 667 | {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, 668 | {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, 669 | {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, 670 | {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, 671 | ] 672 | regex = [ 673 | {file = "regex-2020.2.20-cp27-cp27m-win32.whl", hash = "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb"}, 674 | {file = "regex-2020.2.20-cp27-cp27m-win_amd64.whl", hash = "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74"}, 675 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400"}, 676 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0"}, 677 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc"}, 678 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0"}, 679 | {file = "regex-2020.2.20-cp36-cp36m-win32.whl", hash = "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69"}, 680 | {file = "regex-2020.2.20-cp36-cp36m-win_amd64.whl", hash = "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b"}, 681 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e"}, 682 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242"}, 683 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce"}, 684 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab"}, 685 | {file = "regex-2020.2.20-cp37-cp37m-win32.whl", hash = "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431"}, 686 | {file = "regex-2020.2.20-cp37-cp37m-win_amd64.whl", hash = "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1"}, 687 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_i686.whl", hash = "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045"}, 688 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26"}, 689 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2"}, 690 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70"}, 691 | {file = "regex-2020.2.20-cp38-cp38-win32.whl", hash = "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d"}, 692 | {file = "regex-2020.2.20-cp38-cp38-win_amd64.whl", hash = "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa"}, 693 | {file = "regex-2020.2.20.tar.gz", hash = "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5"}, 694 | ] 695 | requests = [ 696 | {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, 697 | {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, 698 | ] 699 | six = [ 700 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 701 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 702 | ] 703 | soupsieve = [ 704 | {file = "soupsieve-2.0-py2.py3-none-any.whl", hash = "sha256:fcd71e08c0aee99aca1b73f45478549ee7e7fc006d51b37bec9e9def7dc22b69"}, 705 | {file = "soupsieve-2.0.tar.gz", hash = "sha256:e914534802d7ffd233242b785229d5ba0766a7f487385e3f714446a07bf540ae"}, 706 | ] 707 | toml = [ 708 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 709 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 710 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 711 | ] 712 | typed-ast = [ 713 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 714 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 715 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 716 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 717 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 718 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 719 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 720 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 721 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 722 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 723 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 724 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 725 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 726 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 727 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 728 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 729 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 730 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 731 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 732 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 733 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 734 | ] 735 | typing = [ 736 | {file = "typing-3.7.4.1-py2-none-any.whl", hash = "sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36"}, 737 | {file = "typing-3.7.4.1-py3-none-any.whl", hash = "sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"}, 738 | {file = "typing-3.7.4.1.tar.gz", hash = "sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23"}, 739 | ] 740 | urllib3 = [ 741 | {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, 742 | {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, 743 | ] 744 | virtualenv = [ 745 | {file = "virtualenv-20.0.15-py2.py3-none-any.whl", hash = "sha256:4e399f48c6b71228bf79f5febd27e3bbb753d9d5905776a86667bc61ab628a25"}, 746 | {file = "virtualenv-20.0.15.tar.gz", hash = "sha256:9e81279f4a9d16d1c0654a127c2c86e5bca2073585341691882c1e66e31ef8a5"}, 747 | ] 748 | wcwidth = [ 749 | {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, 750 | {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, 751 | ] 752 | zipp = [ 753 | {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, 754 | {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, 755 | ] 756 | --------------------------------------------------------------------------------