diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..491deae --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/qencode-ci.yml b/.github/workflows/qencode-ci.yml new file mode 100644 index 0000000..9b41f45 --- /dev/null +++ b/.github/workflows/qencode-ci.yml @@ -0,0 +1,35 @@ +name: qencode CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ '2.x', '3.x' ] + steps: + - uses: actions/checkout@v1 + - name: Configure git + run: | + git config --global user.name 'travis-ci' + git config --global user.email 'travis@nowhere.edu' + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade 'poetry<1.1' + poetry install + - name: Lint with flake8 + run: | + poetry run flake8 + - name: Test with pytest + run: | + poetry run py.test --cov=./ --cov-report=xml + - uses: codecov/codecov-action@v1 + continue-on-error: true + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 5f8c625..de9133d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,156 @@ *.log *.sql -*.pyc .cache/ .vscode/ -.idea/ \ No newline at end of file +.idea/ +.vim/ +LONG_DESCRIPTION.md + +# +# GitHub's .gitignore for python projects +# https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore +# + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Pip +pip-wheel-metadata/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9ccd720 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +PY_FILES= find . -type f -not -path '*/\.*' | grep -i '.*[.]py$$' 2> /dev/null + + +entr_warn: + @echo "----------------------------------------------------------" + @echo " ! File watching functionality non-operational ! " + @echo " " + @echo "Install entr(1) to automatically run tasks on file change." + @echo "See http://entrproject.org/ " + @echo "----------------------------------------------------------" + +isort: + isort `${PY_FILES}` + +black: + black `${PY_FILES}` --skip-string-normalization + +test: + py.test $(test) + +watch_test: + if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) test; else $(MAKE) test entr_warn; fi + +build_docs: + cd doc && $(MAKE) html + +watch_docs: + cd doc && $(MAKE) watch_docs + +flake8: + flake8 setup.py sample-code qencode tests + +watch_flake8: + if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) flake8; else $(MAKE) flake8 entr_warn; fi diff --git a/README.md b/README.md index 181fa9c..422ef20 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,92 @@ -## qencode-api-python-client +# Qencode Python Library +The Qencode Python library provides convenient access to the Qencode API +for applications written in the Python language. -**install sdk libraries from github** +Inside this repository in sample-code/, there are examples of [video transcoding](https://cloud.qencode.com/) +tasks, launching encoding jobs, video clipping and receiving callbacks. Updates are posted on a regular basis and we are open to any improvements or suggestions you may have. -```` +## Installation + +You don't need the source code unless you want to modify the package itself. +If you just want to use the package: + +```sh +pip install --upgrade qencode +``` + +## Contributing + +To modify the package, install [poetry](https://python-poetry.org/docs/#installation) +and checkout the source: + +```sh cd your-workspace-folder git clone https://github.com/qencode-dev/qencode-api-python-client cd qencode-api-python-client -pip install -r requirements.txt -python setup.py install -```` -**install from pip** - -```` -sudo pip install qencode -```` +poetry shell +poetry install +``` -**Usage** +## Usage -```` +```python import qencode -client = qencode.client(API_KEY) +client = qencode.Client(api_key=API_KEY) client.create() task = client.create_task() task.start(TRANSCODING_PROFILEID, VIDEO_URL) - -#getting video metadata: +# Getting video metadata metadata = client.get_metadata(VIDEO_URL) +``` -```` - -**Documentation** +## Documentation Documentation is available at -**Description** - -Inside this library, you will find sample code for creating [video transcoding](https://cloud.qencode.com/) tasks, launching encoding jobs, video clipping and receiving callbacks. Updates are posted on a regular basis and we are open to any improvements or suggestions you may have. +## Features Some of the options Qencode offers for transcoding your videos at scale: Resolution - * 8K - * 4K - * 1440p - * 1080p - * 720p - * 480p - * 360p - * 240 - -Features - * Thumbnails - * Watermarking - * VR / 360 Encoding - * Subtitles & Captions - * Create Clips - * Video Stitching - * S3 Storage - * Preview Images - * Custom Resolution - * Callback URLs - * Custom Presets - * Rotate - * Aspect Ratio - * Notifications - * Crop Videos + +- 8K +- 4K +- 1440p +- 1080p +- 720p +- 480p +- 360p +- 240 + +Features + +- Thumbnails +- Watermarking +- VR / 360 Encoding +- Subtitles & Captions +- Create Clips +- Video Stitching +- S3 Storage +- Preview Images +- Custom Resolution +- Callback URLs +- Custom Presets +- Rotate +- Aspect Ratio +- Notifications +- Crop Videos Transfer & Storage Options - * S3 Qencode - * AWS - * Google Cloud - * Backblaze - * Azure - * FTP - * HTTP(S) - * VPN \ No newline at end of file + +- S3 Qencode +- AWS +- Google Cloud +- Backblaze +- Azure +- FTP +- HTTP(S) +- VPN diff --git a/docs/install.md b/docs/install.md index b980183..a7e3b94 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,17 +1,21 @@ -##Installation +# Installation -**install sdk libraries from github** +You don't need the source code unless you want to modify the package itself. +If you just want to use the package: -```` +```sh +pip install --upgrade qencode +``` + +# Contributing + +To modify the package, install [poetry](https://python-poetry.org/docs/#installation) +and checkout the source: + +```sh cd your-workspace-folder git clone https://github.com/qencode-dev/qencode-api-python-client cd qencode-api-python-client -pip install -r requirements.txt -python setup.py install -```` - -**install from pip** - -```` -sudo pip install qencode -```` \ No newline at end of file +poetry shell +poetry install +``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 5a6bdaa..86e690c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,35 +1,38 @@ +# Quickstart + ## Installation -**install sdk libraries from github** +You don't need the source code unless you want to modify the package itself. +If you just want to use the package: + +```sh +pip install --upgrade qencode +``` + +# Contributing + +To modify the package, install [poetry](https://python-poetry.org/docs/#installation) +and checkout the source: -```` +```sh cd your-workspace-folder git clone https://github.com/qencode-dev/qencode-api-python-client cd qencode-api-python-client -pip install -r requirements.txt -python setup.py install -```` +poetry shell +poetry install +``` -**install from pip** +## Usage -```` -sudo pip install qencode -```` - -**Usage** - -```` +```python import qencode -client = qencode.client(API_KEY) +client = qencode.Client(api_key=API_KEY) client.create() task = client.create_task() -task.start(TRANSCODING_PROFILEID, VIDO_URL) - -```` - +``` -**Documentation** +## Documentation -Documentation is available at \ No newline at end of file +Documentation is available at diff --git a/docs/usage.md b/docs/usage.md index 89c99b6..a493b34 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,81 +1,73 @@ -##Usage +# Usage -**Usage by transcoding profile ID** +## Usage by transcoding profile ID -```` -import sys +```python import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode +import sys import time +import qencode API_KEY = 'Your API KEY' TRANSCODING_PROFILEID = 'Your profile ID' -VIDO_URL = 'your source url' - +VIDEO_URL = 'your source url' def start_encode(): - """ - Create client object - :param api_key: string. required - :param api_url: string. not required - :param api_version: int. not required. default 'v1' - :return: client object - """ - client = qencode.client(API_KEY) - client.create() - if client.error: - print 'encoder error:', client.error, client.message - raise SystemExit - - """ - :return: task object - """ - task = client.create_task() - task.start_time = 0.0 - task.duration = 10.0 - task.start(TRANSCODING_PROFILEID, VIDO_URL) - if task.error: - print 'task error:', task.error, task.message - raise SystemExit - - while True: - status = task.status() - print '{0} | {1} | {2} | error: {3}'.format(VIDO_URL, - status.get('status'), - status.get('percent'), - status.get('error'), - status.get('error_description')) - if status['error']: - break - if status['status'] == 'completed': - break - time.sleep(15) + client = qencode.Client(api_key=API_KEY) + client.create() + if client.error: + print 'encoder error:', client.error, client.message + raise SystemExit + + task = client.create_task() + task.start_time = 0.0 + task.duration = 10.0 + task.start(TRANSCODING_PROFILEID, VIDEO_URL) + if task.error: + print 'task error:', task.error, task.message + raise SystemExit + + while True: + status = task.status() + print ( + '{0} | {1} | {2} | error: {3}'.format( + VIDEO_URL, + status.get('status'), + status.get('percent'), + status.get('error'), + status.get('error_description'), + ) + ) + if status['error']: + break + if status['status'] == 'completed': + break + time.sleep(15) if __name__ == '__main__': - start_encode() -```` + start_encode() +``` -**Usage by custom parameters** +## Usage by custom parameters -```` -import sys +```python import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode +import sys import time +import qencode + API_KEY = 'Your API KEY' -params = qencode.custom_params() +params = qencode.CustomTranscodingParams() -FORMAT = qencode.format() -STREAM = qencode.stream() -DESTINATION = qencode.destination() -VIDEO_CODEC = qencode.x264_video_codec() +FORMAT = qencode.Format() +STREAM = qencode.Stream() +DESTINATION = qencode.Destination() +VIDEO_CODEC = qencode.Libx264_VideoCodecParameters() DESTINATION.url = "..." @@ -103,67 +95,72 @@ params.format = [FORMAT] def start_encode(): - - """ + """ Create client object :param api_key: string. required :param api_url: string. not required :param api_version: int. not required. default 'v1' :return: client object - """ - client = qencode.client(API_KEY) - client.create() - if client.error: - print 'encoder error:', client.error, client.message - raise SystemExit - - """ + """ + client = qencode.Client(api_key=API_KEY) + client.create() + if client.error: + print('encoder error:', client.error, client.message) + raise SystemExit + + """ Create task :return: task object - """ + """ + task = client.create_task() + task.custom_start(params) + if task.error: + print('task error:', task.error, task.message) + raise SystemExit + + while True: + status = task.status() + print( + '{0} | {1} | {2} | error: {3}'.format( + params.source, + status.get('status'), + status.get('percent'), + status.get('error'), + status.get('error_description'), + ) + ) + if status['error']: + break + if status['status'] == 'completed': + break + time.sleep(15) - task = client.create_task() - task.custom_start(params) - if task.error: - print 'task error:', task.error, task.message - raise SystemExit - while True: - status = task.status() - print '{0} | {1} | {2} | error: {3}'.format(params.source, - status.get('status'), - status.get('percent'), - status.get('error'), - status.get('error_description')) - if status['error']: - break - if status['status'] == 'completed': - break - time.sleep(15) +if __name__ == '__main__': + start_encode() +``` +## Usage with callback methods -if __name__ == '__main__': - start_encode() -```` -**Usage with callback methods** +``` +def on_process_changed(process_data): + print('on_process_changed', process_data) -```` -def my_callback(e): - print e -def my_callback2(e): - print e - -... +def on_task_completed(process_data): + print('on_task_completed', process_data) -task.start(TRANSCODING_PROFILEID, VIDO_URL) -if task.error: - raise SystemExit + +# ... + +task.start(TRANSCODING_PROFILEID, VIDEO_URL) +if task.error: + raise SystemExit task.progress_changed(my_callback) -task.task_completed(my_callback2) -```` +task.task_completed(on_task_completed) +``` -**Documentation** +## Documentation Documentation is available at diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..2208a68 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1426 @@ +[[package]] +category = "dev" +description = "A configurable sidebar-enabled Sphinx theme" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "alabaster" +optional = false +python-versions = "*" +version = "0.7.12" + +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +marker = "python_version >= \"3.6\" and python_version < \"4.0\"" +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "python_version < \"3\" or python_version >= \"3\" and sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +marker = "python_version < \"3\" or python_version >= \"3\" or python_version >= \"3.6\" and python_version < \"4.0\"" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "Internationalization utilities" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "babel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.0" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +category = "dev" +description = "Backport of functools.lru_cache" +marker = "python_version < \"3.2\"" +name = "backports.functools-lru-cache" +optional = false +python-versions = ">=2.6" +version = "1.6.1" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov"] + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +marker = "python_version >= \"3.6\" and python_version < \"4.0\"" +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.10b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" +toml = ">=0.9.4" +typed-ast = ">=1.4.0" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "dev" +description = "An easy safelist-based HTML-sanitizing tool." +name = "bleach" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "3.1.5" + +[package.dependencies] +packaging = "*" +six = ">=1.9.0" +webencodings = "*" + +[[package]] +category = "dev" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.6.20" + +[[package]] +category = "dev" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +marker = "python_version >= \"3.6\" and python_version < \"4.0\"" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "dev" +description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" +name = "codecov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.1.7" + +[package.dependencies] +coverage = "*" +requests = ">=2.7.9" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\" and python_version < \"3\" or python_version >= \"3\" and sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Updated configparser from Python 3.7 for Python 2.6+." +marker = "python_version < \"3.2\"" +name = "configparser" +optional = false +python-versions = ">=2.6" +version = "4.0.2" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8", "pytest-black-multipy"] + +[[package]] +category = "dev" +description = "Backports and enhancements for the contextlib module" +marker = "python_version < \"3\" or python_version >= \"3\" and python_version < \"3.4\"" +name = "contextlib2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.6.0.post1" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.2" + +[package.extras] +toml = ["toml"] + +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.16" + +[[package]] +category = "dev" +description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" +marker = "python_version < \"3.4\"" +name = "enum34" +optional = false +python-versions = "*" +version = "1.1.10" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8 pyflakes and co" +name = "flake8" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.3" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[package.dependencies.configparser] +python = "<3.2" +version = "*" + +[package.dependencies.enum34] +python = "<3.4" +version = "*" + +[package.dependencies.functools32] +python = "<3.2" +version = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[package.dependencies.typing] +python = "<3.5" +version = "*" + +[[package]] +category = "dev" +description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" +marker = "python_version < \"3\"" +name = "funcsigs" +optional = false +python-versions = "*" +version = "1.0.2" + +[[package]] +category = "dev" +description = "Backport of the functools module from Python 3.2.3 for use on 2.7 and PyPy." +marker = "python_version < \"3.2\"" +name = "functools32" +optional = false +python-versions = "*" +version = "3.2.3-2" + +[[package]] +category = "dev" +description = "Backport of the concurrent.futures package from Python 3" +marker = "python_version < \"3.2\"" +name = "futures" +optional = false +python-versions = ">=2.6, <3" +version = "3.3.0" + +[[package]] +category = "dev" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.10" + +[[package]] +category = "dev" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "imagesize" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.2.0" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\" or python_version >= \"3\" and python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.7.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.dependencies.configparser] +python = "<3" +version = ">=3.5" + +[package.dependencies.contextlib2] +python = "<3" +version = "*" + +[package.dependencies.pathlib2] +python = "<3" +version = "*" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[package.dependencies] +[package.dependencies."backports.functools-lru-cache"] +python = "<3.2" +version = "*" + +[package.dependencies.futures] +python = "<3.2" +version = "*" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + +[[package]] +category = "dev" +description = "A very fast and expressive template engine." +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "jinja2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.2" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "dev" +description = "Safely add untrusted strings to HTML/XML markup." +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "Rolling backport of unittest.mock for all Pythons" +marker = "python_version < \"3\"" +name = "mock" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.0.5" + +[package.dependencies] +six = "*" + +[package.dependencies.funcsigs] +python = "<3.3" +version = ">=1" + +[package.extras] +build = ["twine", "wheel", "blurb"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +marker = "python_version <= \"2.7\"" +name = "more-itertools" +optional = false +python-versions = "*" +version = "5.0.0" + +[package.dependencies] +six = ">=1.0.0,<2.0.0" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +marker = "python_version >= \"3\"" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.4.0" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "Object-oriented filesystem paths" +marker = "python_version < \"3\"" +name = "pathlib2" +optional = false +python-versions = "*" +version = "2.3.4" + +[package.dependencies] +six = "*" + +[package.dependencies.scandir] +python = "<3.5" +version = "*" + +[[package]] +category = "dev" +description = "Object-oriented filesystem paths" +marker = "python_version >= \"3\" and python_version < \"3.6\"" +name = "pathlib2" +optional = false +python-versions = "*" +version = "2.3.5" + +[package.dependencies] +six = "*" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +marker = "python_version >= \"3.6\" and python_version < \"4.0\"" +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" + +[[package]] +category = "dev" +description = "Query metadatdata from sdists / bdists / installed packages." +name = "pkginfo" +optional = false +python-versions = "*" +version = "1.5.0.1" + +[package.extras] +testing = ["nose", "coverage"] + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.9.0" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.6.0" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.2.0" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.5.2" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=3.5" +version = "2.6.1" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "pytest" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "4.6.11" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +six = ">=1.10.0" +wcwidth = "*" + +[package.dependencies.colorama] +python = "<3.4.0 || >=3.5.0" +version = "*" + +[package.dependencies.funcsigs] +python = "<3.0" +version = ">=1.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.dependencies.more-itertools] +python = "<2.8" +version = ">=4.0.0,<6.0.0" + +[package.dependencies.pathlib2] +python = "<3.6" +version = ">=2.2.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +marker = "python_version >= \"3\"" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.4.3" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.dependencies.pathlib2] +python = "<3.6" +version = ">=2.2.0" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.9.0" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=3.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +marker = "python_version >= \"3\"" +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.10.0" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] + +[[package]] +category = "dev" +description = "Thin-wrapper around the mock package for easier use with py.test" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "pytest-mock" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.0.0" + +[package.dependencies] +pytest = ">=2.7" + +[package.dependencies.mock] +python = "<3.0" +version = "*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "Thin-wrapper around the mock package for easier use with pytest" +marker = "python_version >= \"3\"" +name = "pytest-mock" +optional = false +python-versions = ">=3.5" +version = "3.1.1" + +[package.dependencies] +pytest = ">=2.7" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + +[[package]] +category = "dev" +description = "World timezone definitions, modern and historical" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "pytz" +optional = false +python-versions = "*" +version = "2020.1" + +[[package]] +category = "dev" +description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" +name = "readme-renderer" +optional = false +python-versions = "*" +version = "26.0" + +[package.dependencies] +Pygments = ">=2.5.1" +bleach = ">=2.1.0" +docutils = ">=0.13.1" +six = "*" + +[package.extras] +md = ["cmarkgfm (>=0.2.0)"] + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +marker = "python_version >= \"3.6\" and python_version < \"4.0\"" +name = "regex" +optional = false +python-versions = "*" +version = "2020.6.8" + +[[package]] +category = "dev" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.24.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "dev" +description = "A utility belt for advanced users of python-requests" +name = "requests-toolbelt" +optional = false +python-versions = "*" +version = "0.9.1" + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +category = "dev" +description = "scandir, a better directory iterator and faster os.walk()" +marker = "python_version < \"3\"" +name = "scandir" +optional = false +python-versions = "*" +version = "1.10.0" + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "dev" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "2.0.0" + +[[package]] +category = "dev" +description = "Python documentation generator" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "sphinx" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.5" + +[package.dependencies] +Jinja2 = ">=2.3" +Pygments = ">=2.0" +alabaster = ">=0.7,<0.8" +babel = ">=1.3,<2.0 || >2.0" +colorama = ">=0.3.5" +docutils = ">=0.11" +imagesize = "*" +packaging = "*" +requests = ">=2.0.0" +setuptools = "*" +six = ">=1.5" +snowballstemmer = ">=1.1" +sphinxcontrib-websupport = "*" + +[package.dependencies.typing] +python = "<3.5" +version = "*" + +[package.extras] +test = ["mock", "pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "enum34", "mypy", "typed-ast"] +websupport = ["sqlalchemy (>=0.9)", "whoosh (>=2.0)"] + +[[package]] +category = "dev" +description = "Python documentation generator" +marker = "python_version >= \"3\"" +name = "sphinx" +optional = false +python-versions = ">=3.5" +version = "3.1.2" + +[package.dependencies] +Jinja2 = ">=2.3" +Pygments = ">=2.0" +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = ">=0.3.5" +docutils = ">=0.12" +imagesize = "*" +packaging = "*" +requests = ">=2.5.0" +setuptools = "*" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] + +[[package]] +category = "dev" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +marker = "python_version >= \"3\"" +name = "sphinxcontrib-applehelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +marker = "python_version >= \"3\"" +name = "sphinxcontrib-devhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +marker = "python_version >= \"3\"" +name = "sphinxcontrib-htmlhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +category = "dev" +description = "A sphinx extension which renders display math in HTML via JavaScript" +marker = "python_version >= \"3\"" +name = "sphinxcontrib-jsmath" +optional = false +python-versions = ">=3.5" +version = "1.0.1" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +marker = "python_version >= \"3\"" +name = "sphinxcontrib-qthelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +marker = "python_version >= \"3\"" +name = "sphinxcontrib-serializinghtml" +optional = false +python-versions = ">=3.5" +version = "1.1.4" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "Sphinx API for Web Apps" +marker = "python_version < \"3\"" +name = "sphinxcontrib-websupport" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.2" + +[package.extras] +test = ["pytest", "mock"] + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +marker = "python_version >= \"3.6\" and python_version < \"4.0\"" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + +[[package]] +category = "dev" +description = "Fast, Extensible Progress Meter" +name = "tqdm" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "4.47.0" + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] + +[[package]] +category = "dev" +description = "Collection of utilities for publishing packages on PyPI" +name = "twine" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.15.0" + +[package.dependencies] +pkginfo = ">=1.4.2" +readme-renderer = ">=21.0" +requests = ">=2.5.0,<2.15 || >2.15,<2.16 || >2.16" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +setuptools = ">=0.7.0" +tqdm = ">=4.14" + +[package.extras] +keyring = ["keyring"] +with-blake2 = ["pyblake2"] + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +marker = "python_version >= \"3.6\" and python_version < \"4.0\"" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "dev" +description = "Type Hints for Python" +marker = "python_version < \"3.5\"" +name = "typing" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <3.5" +version = "3.7.4.2" + +[[package]] +category = "dev" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.9" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "dev" +description = "Measures the displayed width of unicode strings in a terminal" +marker = "python_version < \"3\" or python_version >= \"3\"" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.2.5" + +[package.dependencies] +[package.dependencies."backports.functools-lru-cache"] +python = "<3.2" +version = ">=1.2.1" + +[[package]] +category = "dev" +description = "Character encoding aliases for legacy web content" +name = "webencodings" +optional = false +python-versions = "*" +version = "0.5.1" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\" or python_version >= \"3\" and python_version < \"3.8\" or python_version >= \"3\" and python_version < \"3.8\" and (python_version < \"3.8\" or python_version >= \"3\" and python_version < \"3.8\")" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "1.2.0" + +[package.dependencies] +[package.dependencies.contextlib2] +python = "<3.4" +version = "*" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "5545dfabdca177c9c6444e7933b99c1f1055dd7aec08a139564ed6f9171028cf" +python-versions = "~2.7 || ^3.5" + +[metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, +] +"backports.functools-lru-cache" = [ + {file = "backports.functools_lru_cache-1.6.1-py2.py3-none-any.whl", hash = "sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848"}, + {file = "backports.functools_lru_cache-1.6.1.tar.gz", hash = "sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a"}, +] +black = [ + {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, + {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, +] +bleach = [ + {file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"}, + {file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"}, +] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +codecov = [ + {file = "codecov-2.1.7-py2.py3-none-any.whl", hash = "sha256:b67bb8029e8340a7bf22c71cbece5bd18c96261fdebc2f105ee4d5a005bc8728"}, + {file = "codecov-2.1.7-py3.8.egg", hash = "sha256:d8b8109f44edad03b24f5f189dac8de9b1e3dc3c791fa37eeaf8c7381503ec34"}, + {file = "codecov-2.1.7.tar.gz", hash = "sha256:491938ad774ea94a963d5d16354c7299e90422a33a353ba0d38d0943ed1d5091"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +configparser = [ + {file = "configparser-4.0.2-py2.py3-none-any.whl", hash = "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c"}, + {file = "configparser-4.0.2.tar.gz", hash = "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"}, +] +contextlib2 = [ + {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, + {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, +] +coverage = [ + {file = "coverage-5.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a"}, + {file = "coverage-5.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10"}, + {file = "coverage-5.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62"}, + {file = "coverage-5.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613"}, + {file = "coverage-5.2-cp27-cp27m-win32.whl", hash = "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4"}, + {file = "coverage-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a"}, + {file = "coverage-5.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70"}, + {file = "coverage-5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee"}, + {file = "coverage-5.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b"}, + {file = "coverage-5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913"}, + {file = "coverage-5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c"}, + {file = "coverage-5.2-cp35-cp35m-win32.whl", hash = "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b"}, + {file = "coverage-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e"}, + {file = "coverage-5.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0"}, + {file = "coverage-5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f"}, + {file = "coverage-5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405"}, + {file = "coverage-5.2-cp36-cp36m-win32.whl", hash = "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40"}, + {file = "coverage-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e"}, + {file = "coverage-5.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6"}, + {file = "coverage-5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1"}, + {file = "coverage-5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d"}, + {file = "coverage-5.2-cp37-cp37m-win32.whl", hash = "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec"}, + {file = "coverage-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703"}, + {file = "coverage-5.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032"}, + {file = "coverage-5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d"}, + {file = "coverage-5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e"}, + {file = "coverage-5.2-cp38-cp38-win32.whl", hash = "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7"}, + {file = "coverage-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"}, + {file = "coverage-5.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d"}, + {file = "coverage-5.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d"}, + {file = "coverage-5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c"}, + {file = "coverage-5.2-cp39-cp39-win32.whl", hash = "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c"}, + {file = "coverage-5.2-cp39-cp39-win_amd64.whl", hash = "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2"}, + {file = "coverage-5.2.tar.gz", hash = "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +enum34 = [ + {file = "enum34-1.1.10-py2-none-any.whl", hash = "sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53"}, + {file = "enum34-1.1.10-py3-none-any.whl", hash = "sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328"}, + {file = "enum34-1.1.10.tar.gz", hash = "sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248"}, +] +flake8 = [ + {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, + {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, +] +funcsigs = [ + {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, + {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, +] +functools32 = [ + {file = "functools32-3.2.3-2.tar.gz", hash = "sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d"}, + {file = "functools32-3.2.3-2.zip", hash = "sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0"}, +] +futures = [ + {file = "futures-3.3.0-py2-none-any.whl", hash = "sha256:49b3f5b064b6e3afc3316421a3f25f66c137ae88f068abbf72830170033c5e16"}, + {file = "futures-3.3.0.tar.gz", hash = "sha256:7e033af76a5e35f58e56da7a91e687706faf4e7bdfb2cbc3f2cca6b9bcda9794"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, + {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mock = [ + {file = "mock-3.0.5-py2.py3-none-any.whl", hash = "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"}, + {file = "mock-3.0.5.tar.gz", hash = "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3"}, +] +more-itertools = [ + {file = "more-itertools-5.0.0.tar.gz", hash = "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4"}, + {file = "more_itertools-5.0.0-py2-none-any.whl", hash = "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc"}, + {file = "more_itertools-5.0.0-py3-none-any.whl", hash = "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"}, + {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, + {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pathlib2 = [ + {file = "pathlib2-2.3.4-py2.py3-none-any.whl", hash = "sha256:2156525d6576d21c4dcaddfa427fae887ef89a7a9de5cbfe0728b3aafa78427e"}, + {file = "pathlib2-2.3.4.tar.gz", hash = "sha256:446014523bb9be5c28128c4d2a10ad6bb60769e78bd85658fe44a450674e0ef8"}, + {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, + {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, +] +pkginfo = [ + {file = "pkginfo-1.5.0.1-py2.py3-none-any.whl", hash = "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"}, + {file = "pkginfo-1.5.0.1.tar.gz", hash = "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, + {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pygments = [ + {file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, + {file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, + {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-4.6.11-py2.py3-none-any.whl", hash = "sha256:a00a7d79cbbdfa9d21e7d0298392a8dd4123316bfac545075e6f8f24c94d8c97"}, + {file = "pytest-4.6.11.tar.gz", hash = "sha256:50fa82392f2120cc3ec2ca0a75ee615be4c479e66669789771f1758332be4353"}, + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] +pytest-cov = [ + {file = "pytest-cov-2.9.0.tar.gz", hash = "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322"}, + {file = "pytest_cov-2.9.0-py2.py3-none-any.whl", hash = "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424"}, + {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, + {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, +] +pytest-mock = [ + {file = "pytest-mock-2.0.0.tar.gz", hash = "sha256:b35eb281e93aafed138db25c8772b95d3756108b601947f89af503f8c629413f"}, + {file = "pytest_mock-2.0.0-py2.py3-none-any.whl", hash = "sha256:cb67402d87d5f53c579263d37971a164743dc33c159dfb4fb4a86f37c5552307"}, + {file = "pytest-mock-3.1.1.tar.gz", hash = "sha256:636e792f7dd9e2c80657e174c04bf7aa92672350090736d82e97e92ce8f68737"}, + {file = "pytest_mock-3.1.1-py3-none-any.whl", hash = "sha256:a9fedba70e37acf016238bb2293f2652ce19985ceb245bbd3d7f3e4032667402"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +readme-renderer = [ + {file = "readme_renderer-26.0-py2.py3-none-any.whl", hash = "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376"}, + {file = "readme_renderer-26.0.tar.gz", hash = "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d"}, +] +regex = [ + {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, + {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, + {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, + {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, + {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, + {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, + {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, + {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, + {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +requests-toolbelt = [ + {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, + {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, +] +scandir = [ + {file = "scandir-1.10.0-cp27-cp27m-win32.whl", hash = "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188"}, + {file = "scandir-1.10.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"}, + {file = "scandir-1.10.0-cp34-cp34m-win32.whl", hash = "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f"}, + {file = "scandir-1.10.0-cp34-cp34m-win_amd64.whl", hash = "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e"}, + {file = "scandir-1.10.0-cp35-cp35m-win32.whl", hash = "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f"}, + {file = "scandir-1.10.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32"}, + {file = "scandir-1.10.0-cp36-cp36m-win32.whl", hash = "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022"}, + {file = "scandir-1.10.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4"}, + {file = "scandir-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173"}, + {file = "scandir-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d"}, + {file = "scandir-1.10.0.tar.gz", hash = "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +sphinx = [ + {file = "Sphinx-1.8.5-py2.py3-none-any.whl", hash = "sha256:9f3e17c64b34afc653d7c5ec95766e03043cc6d80b0de224f59b6b6e19d37c3c"}, + {file = "Sphinx-1.8.5.tar.gz", hash = "sha256:c7658aab75c920288a8cf6f09f244c6cfdae30d82d803ac1634d9f223a80ca08"}, + {file = "Sphinx-3.1.2-py3-none-any.whl", hash = "sha256:97dbf2e31fc5684bb805104b8ad34434ed70e6c588f6896991b2fdfd2bef8c00"}, + {file = "Sphinx-3.1.2.tar.gz", hash = "sha256:b9daeb9b39aa1ffefc2809b43604109825300300b987a24f45976c001ba1a8fd"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] +sphinxcontrib-websupport = [ + {file = "sphinxcontrib-websupport-1.1.2.tar.gz", hash = "sha256:1501befb0fdf1d1c29a800fdbf4ef5dc5369377300ddbdd16d2cd40e54c6eefc"}, + {file = "sphinxcontrib_websupport-1.1.2-py2.py3-none-any.whl", hash = "sha256:e02f717baf02d0b6c3dd62cf81232ffca4c9d5c331e03766982e3ff9f1d2bc3f"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +tqdm = [ + {file = "tqdm-4.47.0-py2.py3-none-any.whl", hash = "sha256:7810e627bcf9d983a99d9ff8a0c09674400fd2927eddabeadf153c14a2ec8656"}, + {file = "tqdm-4.47.0.tar.gz", hash = "sha256:63ef7a6d3eb39f80d6b36e4867566b3d8e5f1fe3d6cb50c5e9ede2b3198ba7b7"}, +] +twine = [ + {file = "twine-1.15.0-py2.py3-none-any.whl", hash = "sha256:630fadd6e342e725930be6c696537e3f9ccc54331742b16245dab292a17d0460"}, + {file = "twine-1.15.0.tar.gz", hash = "sha256:a3d22aab467b4682a22de4a422632e79d07eebd07ff2a7079effb13f8a693787"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typing = [ + {file = "typing-3.7.4.2-py2-none-any.whl", hash = "sha256:e470592ab1725d6810cd91d687c857a853d136d9a9dc9289713fa887f39b931a"}, + {file = "typing-3.7.4.2.tar.gz", hash = "sha256:6f6f3553709d2234e412092e5daa93aaaaa42ea1854505442280b39f9311707f"}, +] +urllib3 = [ + {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, + {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] +zipp = [ + {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, + {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, +] diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7ec1be8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[tool.black] +skip-string-normalization = true + +[tool.poetry] +name = "qencode" +version = "1.0.0" +description = "Python bindings for the Qencode API" +authors = ["Qencode Team "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" + +[tool.poetry.dev-dependencies] +black = {version="==19.10b0", python="^3.6"} +docutils = "*" +flake8 = "*" +isort = "<5" +pytest = [ + {version="<4.7.0", python="<3"}, + {version="*", python=">=3"} +] +pathlib2 = {version="<2.3.5", python="<3"} # Untangle pytest peer-dependency +sphinx = [ + {version="<2", python="<3"}, + {version="*", python=">=3"} +] +twine = "*" +codecov = "*" +coverage = "*" +pytest-cov = [ + {version="<2.10.0", python="<3"}, + {version="*", python=">=3"} +] +pytest-mock = [ + {version="<3.0.0", python="<3"}, + {version="*", python=">=3"} +] + +[build-system] +requires = ["poetry>=0.12", "setuptools"] +build-backend = "poetry.masonry.api" diff --git a/qencode/__init__.py b/qencode/__init__.py index 7b49092..2593857 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -1,37 +1,21 @@ +from .client import Client +from .custom_params import ( + CustomTranscodingParams, + Destination, + Format, + Libx264_VideoCodecParameters, + Libx265_VideoCodecParameters, + Stream, +) +from .exceptions import QencodeClientException, QencodeTaskException + def client(api_key, api_url=None, version=None, **kwargs): from client import QencodeApiClient - return QencodeApiClient(api_key, api_url=api_url, version=version, **kwargs) - -def custom_params(): - from custom_params import CustomTranscodingParams - return CustomTranscodingParams() - -def format(): - from custom_params import Format - return Format() - -def destination(): - from custom_params import Destination - return Destination() - -def stream(): - from custom_params import Stream - return Stream() -def x264_video_codec(): - from custom_params import Libx264_VideoCodecParameters - return Libx264_VideoCodecParameters() + return Client(api_key, api_url=api_url, version=version, **kwargs) -def x265_video_codec(): - from custom_params import Libx265_VideoCodecParameters - return Libx265_VideoCodecParameters() - -from exeptions import QencodeClientException, QencodeTaskException __version__ = "1.0" __status__ = "Production/Stable" __author__ = "Qencode" - - - diff --git a/qencode/_compat.py b/qencode/_compat.py new file mode 100644 index 0000000..5921b3c --- /dev/null +++ b/qencode/_compat.py @@ -0,0 +1,16 @@ +# flake8: NOQA +import sys + +PY2 = sys.version_info[0] == 2 + + +if PY2: + string_types = (str, unicode) + from urllib import urlencode + from urllib2 import urlopen, Request, HTTPError, URLError + from urlparse import urljoin +else: + string_types = (str,) + from urllib.parse import urlencode, urljoin + from urllib.request import urlopen, Request + from urllib.error import HTTPError, URLError diff --git a/qencode/client.py b/qencode/client.py index 993043c..d7f5f9c 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -1,51 +1,45 @@ -from httptools import Http -from task import Task -from metadata import Metadata - -class QencodeApiClient(object): - - """ - :return: encoder object - - """ - def __init__(self, api_key, api_url=None, version=None): - self.api_key = api_key - self.api_url = api_url if api_url else 'https://api.qencode.com/' - self.version = version if version else 'v1' - self.connect = Http(self.version, self.api_url) - self.access_token = None - self.expire = None - self.error = None - self.code = None - self.message = '' - self._get_access_token() - - - def create_task(self, **kwargs): - return Task(self.access_token, self.connect, **kwargs) - - def refresh_access_token(self): - response = self.connect.request('access_token', dict(api_key=self.api_key)) - if not response['error']: - self.access_token = response['token'] - self.expire = response['expire'] - else: - self.error = response['error'] - self.code = response['error'] - self.message = response.get('message') - - - def _get_access_token(self): - response = self.connect.request('access_token', dict(api_key=self.api_key)) - if not response['error']: - self.access_token = response['token'] - self.expire = response['expire'] - else: - self.error = response['error'] - self.code = response['error'] - self.message = response.get('message') - - def get_metadata(self, uri): - metadata = Metadata(self.access_token, self.connect) - video_info = metadata.get(uri) - return video_info \ No newline at end of file +from .httptools import Http +from .metadata import Metadata +from .task import Task + + +class Client(object): + def __init__(self, api_key, api_url=None, version=None): + self.api_key = api_key + self.api_url = api_url if api_url else 'https://api.qencode.com/' + self.version = version if version else 'v1' + self.connect = Http(self.version, self.api_url) + self.access_token = None + self.expire = None + self.error = None + self.code = None + self.message = '' + self._get_access_token() + + def create_task(self, **kwargs): + return Task(self.access_token, self.connect, **kwargs) + + def refresh_access_token(self): + response = self.connect.request('access_token', dict(api_key=self.api_key)) + if not response['error']: + self.access_token = response['token'] + self.expire = response['expire'] + else: + self.error = response['error'] + self.code = response['error'] + self.message = response.get('message') + + def _get_access_token(self): + response = self.connect.request('access_token', dict(api_key=self.api_key)) + if not response['error']: + self.access_token = response['token'] + self.expire = response['expire'] + else: + self.error = response['error'] + self.code = response['error'] + self.message = response.get('message') + + def get_metadata(self, uri): + metadata = Metadata(self.access_token, self.connect) + video_info = metadata.get(uri) + return video_info diff --git a/qencode/const.py b/qencode/const.py index f9c0e9a..4acc0c3 100644 --- a/qencode/const.py +++ b/qencode/const.py @@ -3,18 +3,18 @@ SLEEP_ERROR = 60 COMPLETED_STATUS = ['completed', 'saved'] -ERROR_OK = 0 -ERROR_SERVER_INTERNAL = 1 -ERROR_BAD_APP_ID = 2 -ERROR_APP_ID_NOT_FOUND = 3 -ERROR_BAD_TOKEN = 4 -ERROR_TOKEN_NOT_FOUND = 5 -ERROR_TARIFF_NOT_PAID = 6 -ERROR_MASTER_NOT_FOUND = 7 -ERROR_SYSTEM_BUSY = 8 -ERROR_BAD_PAYLOAD = 9 +ERROR_OK = 0 +ERROR_SERVER_INTERNAL = 1 +ERROR_BAD_APP_ID = 2 +ERROR_APP_ID_NOT_FOUND = 3 +ERROR_BAD_TOKEN = 4 +ERROR_TOKEN_NOT_FOUND = 5 +ERROR_TARIFF_NOT_PAID = 6 +ERROR_MASTER_NOT_FOUND = 7 +ERROR_SYSTEM_BUSY = 8 +ERROR_BAD_PAYLOAD = 9 ERROR_PROJECT_NOT_FOUND = 10 -ERROR_BAD_PROFILE = 11 +ERROR_BAD_PROFILE = 11 ERROR_PROFILE_NOT_FOUND = 12 -ERROR_BAD_TOKENS = 13 -ERROR_FIELD_REQUIRED = 14 \ No newline at end of file +ERROR_BAD_TOKENS = 13 +ERROR_FIELD_REQUIRED = 14 diff --git a/qencode/custom_params.py b/qencode/custom_params.py index 847f0a7..ac5eec6 100644 --- a/qencode/custom_params.py +++ b/qencode/custom_params.py @@ -1,144 +1,150 @@ import json from json import JSONEncoder -from utils import rm_attributes_if_null + +from ._compat import PY2 +from .utils import rm_attributes_if_null + class CustomTranscodingParams(object): - """CustomTranscodingParams + """CustomTranscodingParams :var source: String. Source video URI. Can be http(s) url or tus uri - :var format: String. A list of objects, each describing params for a single output video stream (MP4, WEBM, HLS or MPEG-DASH) + :var format: String. + A list of objects, each describing params for a single output video stream (MP4, + WEBM, HLS or MPEG-DASH) + """ - """ - def __init__(self): - self.source = None - self.format = None - self.callback_url = None - rm_attributes_if_null(self) + def __init__(self): + self.source = None + self.format = None + self.callback_url = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) - def remove_null_params(self): - rm_attributes_if_null(self) class Format(object): - """ - :var - :var + def __init__(self): + self.output = None + self.file_extension = None + self.destination = None + self.segment_duration = None + self.stream = None + self.logo = None + self.start_time = None + self.duration = None + self.is_watermark = None + self.size = None + self.video_codec = None + self.audio_codec = None + self.aspect_ratio = None + self.quality = None + self.interval = None + self.width = None + self.height = None + self.time = None + self.path = None + self.resize_mod = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) - """ - def __init__(self): - self.output = None - self.file_extension = None - self.destination = None - self.segment_duration = None - self.stream = None - self.logo = None - self.start_time = None - self.duration = None - self.is_watermark = None - self.size = None - self.video_codec = None - self.audio_codec = None - self.aspect_ratio = None - self.quality = None - self.interval = None - self.width = None - self.height = None - self.time = None - self.path = None - self.resize_mod = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) class Destination(object): - def __init__(self): - self.url = None - self.key = None - self.secret = None - self.permissions = None - self.storage_class = None - rm_attributes_if_null(self) + def __init__(self): + self.url = None + self.key = None + self.secret = None + self.permissions = None + self.storage_class = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) - def remove_null_params(self): - rm_attributes_if_null(self) class Stream(object): - def __init__(self): - self.size = None - self.video_codec = None - self.bitrate = None - self.quality = None - self.rotate = None - self.framerate = None - self.pix_format = None - self.profile = None - self.video_codec_parameters = None - self.keyframe = None - self.segment_duration = None - self.start_time = None - self.duration = None - self.audio_bitrate = None - self.audio_sample_rate = None - self.audio_channels_number = None - self.audio_codec = None - self.downmix_mode = None - self.logo = None - self.aspect_ratio = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) + def __init__(self): + self.size = None + self.video_codec = None + self.bitrate = None + self.quality = None + self.rotate = None + self.framerate = None + self.pix_format = None + self.profile = None + self.video_codec_parameters = None + self.keyframe = None + self.segment_duration = None + self.start_time = None + self.duration = None + self.audio_bitrate = None + self.audio_sample_rate = None + self.audio_channels_number = None + self.audio_codec = None + self.downmix_mode = None + self.logo = None + self.aspect_ratio = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) + class Libx264_VideoCodecParameters(object): - def __init__(self): - self.vprofile = None - self.level = None - self.coder = None - self.flags2 = None - self.partitions = None - self.bf = None - self.directpred = None - self.me_method = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) + def __init__(self): + self.vprofile = None + self.level = None + self.coder = None + self.flags2 = None + self.partitions = None + self.bf = None + self.directpred = None + self.me_method = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) + class Libx265_VideoCodecParameters(object): - def __init__(self): - pass + def __init__(self): + pass class MyEncoder(JSONEncoder): - def default(self, obj): - return obj.__dict__ + def default(self, obj): + return obj.__dict__ class Query(object): - def __init__(self): - self.params = None - self.error = None - self.message = '' - self.query = None - - def prepare_params(self): - query = dict(query=self.params) - try: - self.query = json.dumps(query, cls=MyEncoder, encoding='utf-8') - except Exception as e: - self.error = True - self.message = repr(e) - - def validate_params(self): - if not self.params: - self.error = True - self.message = 'Params is required' - return - if not 'source' in self.params.__dict__: - self.error = True - self.message = 'Params: source is required' - return - if not 'format' in self.params.__dict__: - self.error = True - self.message = 'Params: format is required' - return \ No newline at end of file + def __init__(self): + self.params = None + self.error = None + self.message = '' + self.query = None + + def prepare_params(self): + query = dict(query=self.params) + try: + dump_kwargs = {'encoding': 'utf-8'} if PY2 else {} + self.query = json.dumps(query, cls=MyEncoder, **dump_kwargs) + except Exception as e: + self.error = True + self.message = repr(e) + + def validate_params(self): + if not self.params: + self.error = True + self.message = 'Params is required' + return + if 'source' not in self.params.__dict__: + self.error = True + self.message = 'Params: source is required' + return + if 'format' not in self.params.__dict__: + self.error = True + self.message = 'Params: format is required' + return diff --git a/qencode/exceptions.py b/qencode/exceptions.py new file mode 100644 index 0000000..fa5b795 --- /dev/null +++ b/qencode/exceptions.py @@ -0,0 +1,15 @@ +class QencodeException(Exception): + def __init__(self, message, *args): + super(QencodeException, self).__init__(message, *args) + self.error = message + self.arg = [i for i in args] + + +class QencodeClientException(QencodeException): + def __init__(self, message, *args): + super(QencodeClientException, self).__init__(message, *args) + + +class QencodeTaskException(QencodeException): + def __init__(self, message, *args): + super(QencodeTaskException, self).__init__(message, *args) diff --git a/qencode/exeptions.py b/qencode/exeptions.py deleted file mode 100644 index 36b5fdc..0000000 --- a/qencode/exeptions.py +++ /dev/null @@ -1,18 +0,0 @@ -class QencodeException(Exception): - def __init__(self, message, *args): - super(QencodeException, self).__init__(message, *args) - self.error = message - self.arg = [i for i in args] - -class QencodeClientException(QencodeException): - def __init__(self, message, *args): - super(QencodeClientException, self).__init__(message, *args) - - -class QencodeTaskException(QencodeException): - def __init__(self, message, *args): - super(QencodeTaskException, self).__init__(message, *args) - - - - diff --git a/qencode/httptools.py b/qencode/httptools.py index 5a3d89f..5802a93 100644 --- a/qencode/httptools.py +++ b/qencode/httptools.py @@ -1,46 +1,51 @@ +from __future__ import unicode_literals + import json -import urllib -import urllib2 -from urlparse import urljoin + +from ._compat import HTTPError, Request, URLError, urlencode, urljoin, urlopen + class Http(object): - def __init__(self, version, url, debug=False): - self.version = version - self.url = url - self._debug = debug + def __init__(self, version, url, debug=False): + self.version = version + self.url = url + self._debug = debug - def _call_server(self, url, post_data): - if not url: - response = dict(error=True, message='AttributeError: Bad URL') - return json.dumps(response) - data = urllib.urlencode(post_data) - request = urllib2.Request(url, data) - try: - res = urllib2.urlopen(request) - except urllib2.HTTPError as e: - headers = e.headers if self._debug else '' - response = dict(error=True, message='HTTPError: {0} {1} {2}'.format(e.code, e.reason, headers)) - response = json.dumps(response) - except urllib2.URLError as e: - response = dict(error=True, message='URLError: {0}'.format(e.reason)) - response = json.dumps(response) - else: - response = res.read() - return response + def _call_server(self, url, post_data): + if not url: + response = dict(error=True, message='AttributeError: Bad URL') + return json.dumps(response) + data = urlencode(post_data).encode("utf-8") + request = Request(url, data) + try: + res = urlopen(request) + except HTTPError as e: + headers = e.headers if self._debug else '' + response = dict( + error=True, + message='HTTPError: {0} {1} {2}'.format(e.code, e.reason, headers), + ) + response = json.dumps(response) + except URLError as e: + response = dict(error=True, message='URLError: {0}'.format(e.reason)) + response = json.dumps(response) + else: + response = res.read() + return response - def request(self, api_name, data): - path = '{version}/{api_name}'.format(version=self.version, api_name=api_name) - response = self._call_server(urljoin(self.url, path), data) - try: - response = json.loads(response) - except ValueError as e: - response = dict(error=True, message=repr(e)) - return response + def request(self, api_name, data): + path = '{version}/{api_name}'.format(version=self.version, api_name=api_name) + response = self._call_server(urljoin(self.url, path), data) + try: + response = json.loads(response) + except ValueError as e: + response = dict(error=True, message=repr(e)) + return response - def post(self, url, data): - response = self._call_server(url, data) - try: - response = json.loads(response) - except ValueError as e: - response = dict(error=True, message=repr(e)) - return response \ No newline at end of file + def post(self, url, data): + response = self._call_server(url, data) + try: + response = json.loads(response) + except ValueError as e: + response = dict(error=True, message=repr(e)) + return response diff --git a/qencode/metadata.py b/qencode/metadata.py index d91176d..2986448 100644 --- a/qencode/metadata.py +++ b/qencode/metadata.py @@ -1,17 +1,22 @@ -from task import * -from qencode import QencodeTaskException -import urllib2 +import time + +from ._compat import urlopen +from .exceptions import QencodeTaskException +from .task import Task -class Metadata(Task): +class Metadata(Task): def get(self, uri): - params = """ + params = ( + """ {"query": { "source": "%s", "format": [ {"output": "metadata", "metadata_version": "4.1.5"} ] } } - """ % uri + """ + % uri + ) self.custom_start(params) while True: status = self.status() @@ -33,8 +38,6 @@ def get(self, uri): if url is None: raise QencodeTaskException('No metadata URL found in status response') - data = urllib2.urlopen(url).read() + data = urlopen(url).read() return data - - diff --git a/qencode/task.py b/qencode/task.py index c867647..e77f601 100644 --- a/qencode/task.py +++ b/qencode/task.py @@ -1,191 +1,195 @@ -from custom_params import Query, CustomTranscodingParams -from const import * -import time import json -from utils import is_json, rm_key_if_null +import time +from . import const as constants +from ._compat import string_types +from .custom_params import CustomTranscodingParams, Query +from .utils import is_json, rm_key_if_null -class Task(object): - def __init__(self, access_token, connect, debug=False, **kwargs): - self.connect = connect - self.status_url = None - self.main_status_url = '{0}/{1}/status'.format(self.connect.url, self.connect.version) - self.task_token = None - self.upload_url = None - self.access_token = access_token - self._debug = debug - self.message = '' - self.error = None - self.repeat = kwargs.get('repeats') if kwargs.get('repeats') else REPEAT - self._create_task(1) - - - def start(self, profiles, video_url, **kwargs): - """Creating task and starting encode - - :param profiles: String or List object. Profile uuid - :param transfer_method: String. Transfer method uuid - :param video_url: String. Url of source video - :param payload: String. - :return: None - - """ - if not self.error: - # self._create_task(1) - data = self._prepare_data(profiles, video_url, **kwargs) - - if not self.error and self.task_token: - self._start_encode('start_encode', data) - - def custom_start(self, data, **kwargs): - """Creating task and starting encode - - :param query: JSON object for query param. For examples: https://docs.qencode.com - :param payload: String. - :return: None - - """ - if data is None: - self.error = True - self.message = 'Params is required' - - #if not self.error: - # self._create_task(1) - - if not self.error: - query = self._prepare_query(data) - - if not self.error: - data = self._prepare_data_custom(query, **kwargs) - - if not self.error and self.task_token: - self._start_encode('start_encode2', data) - - def status(self): - return self._status() - - def main_status(self): - return self._status2() - - def progress_changed(self, callback, *args, **kwargs): - while 1: - status = self._status() - if status['error']: - return callback(status, *args, **kwargs) - callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - break - time.sleep(SLEEP_REGULAR) - - def task_completed(self, callback, *args, **kwargs): - while 1: - status = self._status() - if status['error']: - return callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - return callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - break - time.sleep(SLEEP_REGULAR) - - def _prepare_query(self, params): - if isinstance(params, CustomTranscodingParams): - query_obj = Query() - query_obj.params = params - query_obj.validate_params() - if query_obj.error: - self.error = query_obj.error - self.message = query_obj.message - query_obj.prepare_params() - if query_obj.error: - self.error = query_obj.error - self.message = query_obj.message - return query_obj.query - - if isinstance(params, dict): - query = rm_key_if_null(params) - return json.dumps(query) - - if isinstance(params, basestring): - if is_json(params): - query = rm_key_if_null(params) - return query - else: - self.error = True - try: - self.message = "JSON is not well formatted: {0} Is not defined".format(params) - except Exception as e: - pass - finally: - self.message = "JSON is not well formatted" - - def _prepare_data(self, profiles, video_url, **kwargs): - data = dict( - task_token=self.task_token, - profiles=', '.join(profiles) if type(profiles).__name__ == 'list' else profiles - ) - if isinstance(video_url, list): - try: - data.update(stitch=json.dumps(video_url)) - except Exception: - data.update(stitch=video_url) - else: - data.update(uri=video_url) - if kwargs: - data.update(kwargs) - return data - - def _prepare_data_custom(self, query_json, **kwargs): - data = dict( - task_token=self.task_token, - query=query_json - ) - if kwargs: - data.update(kwargs) - return data - - def _create_task(self, count): - res = self.connect.request('create_task', dict(token=self.access_token)) - if not res['error']: - self.task_token = res.get('task_token') - self.upload_url = res.get('upload_url') - else: - self.error = res['error'] - self.message = res.get('message') - - if self.error and self.error == 8: - if count < REPEAT: - time.sleep(SLEEP_ERROR) - self._create_task(count + 1) - - - def _start_encode(self, api_name, data): - res = self.connect.request(api_name, data) - if not res['error'] and res.get('status_url'): - self.status_url = res['status_url'] - else: - self.status_url = self.main_status_url - self.error = res.get('error') - self.message = res.get('message') - - def _status(self): - response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) - status = None - - if response['error'] == ERROR_BAD_TOKENS: - raise ValueError('Bad token: ' + str(self.task_token)) - - if 'statuses' in response and self.task_token in response['statuses']: - status = response['statuses'][self.task_token] - - if not status and self.status_url != self.main_status_url: - self.status_url = self.main_status_url - response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) - if 'statuses' in response and self.task_token in response['statuses']: - status = response['statuses'][self.task_token] - - if status and 'status_url' in status: - self.status_url = status['status_url'] - - return status +class Task(object): + def __init__(self, access_token, connect, debug=False, **kwargs): + self.connect = connect + self.status_url = None + self.main_status_url = '{0}/{1}/status'.format( + self.connect.url, self.connect.version + ) + self.task_token = None + self.upload_url = None + self.access_token = access_token + self._debug = debug + self.message = '' + self.error = None + self.repeat = ( + kwargs.get('repeats') if kwargs.get('repeats') else constants.REPEAT + ) + self._create_task(1) + + def start(self, profiles, video_url, **kwargs): + """Creating task and starting encode. + + :param profiles: String or List object. Profile uuid + :param transfer_method: String. Transfer method uuid + :param video_url: String. Url of source video + :param payload: String. + :return: None + """ + if not self.error: + # self._create_task(1) + data = self._prepare_data(profiles, video_url, **kwargs) + + if not self.error and self.task_token: + self._start_encode('start_encode', data) + + def custom_start(self, data, **kwargs): + """Creating task and starting encode + + :param query: + JSON object for query param. For examples: https://docs.qencode.com + :param payload: String. + :return: None + """ + if data is None: + self.error = True + self.message = 'Params is required' + + # if not self.error: + # self._create_task(1) + + if not self.error: + query = self._prepare_query(data) + + if not self.error: + data = self._prepare_data_custom(query, **kwargs) + + if not self.error and self.task_token: + self._start_encode('start_encode2', data) + + def status(self): + return self._status() + + def main_status(self): + return self._status2() + + def progress_changed(self, callback, *args, **kwargs): + while 1: + status = self._status() + if status['error']: + return callback(status, *args, **kwargs) + callback(status, *args, **kwargs) + if status.get('status') in constants.COMPLETED_STATUS: + break + time.sleep(constants.SLEEP_REGULAR) + + def task_completed(self, callback, *args, **kwargs): + while 1: + status = self._status() + if status['error']: + return callback(status, *args, **kwargs) + if status.get('status') in constants.COMPLETED_STATUS: + return callback(status, *args, **kwargs) + if status.get('status') in constants.COMPLETED_STATUS: + break + time.sleep(constants.SLEEP_REGULAR) + + def _prepare_query(self, params): + if isinstance(params, CustomTranscodingParams): + query_obj = Query() + query_obj.params = params + query_obj.validate_params() + if query_obj.error: + self.error = query_obj.error + self.message = query_obj.message + query_obj.prepare_params() + if query_obj.error: + self.error = query_obj.error + self.message = query_obj.message + return query_obj.query + + if isinstance(params, dict): + query = rm_key_if_null(params) + return json.dumps(query) + + if isinstance(params, string_types): + if is_json(params): + query = rm_key_if_null(params) + return query + else: + self.error = True + error_msg = "JSON is not well formatted" + try: + self.message = "{}: {} Is not defined".format(error_msg, params) + except Exception: + pass + finally: + self.message = error_msg + + def _prepare_data(self, profiles, video_url, **kwargs): + data = dict( + task_token=self.task_token, + profiles=', '.join(profiles) + if type(profiles).__name__ == 'list' + else profiles, + ) + if isinstance(video_url, list): + try: + data.update(stitch=json.dumps(video_url)) + except Exception: + data.update(stitch=video_url) + else: + data.update(uri=video_url) + if kwargs: + data.update(kwargs) + return data + + def _prepare_data_custom(self, query_json, **kwargs): + data = dict(task_token=self.task_token, query=query_json) + if kwargs: + data.update(kwargs) + return data + + def _create_task(self, count): + res = self.connect.request('create_task', dict(token=self.access_token)) + if not res['error']: + self.task_token = res.get('task_token') + self.upload_url = res.get('upload_url') + else: + self.error = res['error'] + self.message = res.get('message') + + if self.error and self.error == 8: + if count < constants.REPEAT: + time.sleep(constants.SLEEP_ERROR) + self._create_task(count + 1) + + def _start_encode(self, api_name, data): + res = self.connect.request(api_name, data) + if not res['error'] and res.get('status_url'): + self.status_url = res['status_url'] + else: + self.status_url = self.main_status_url + self.error = res.get('error') + self.message = res.get('message') + + def _status(self): + response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) + status = None + + if response['error'] == constants.ERROR_BAD_TOKENS: + raise ValueError('Bad token: ' + str(self.task_token)) + + if 'statuses' in response and self.task_token in response['statuses']: + status = response['statuses'][self.task_token] + + if not status and self.status_url != self.main_status_url: + self.status_url = self.main_status_url + response = self.connect.post( + self.status_url, dict(task_tokens=self.task_token) + ) + if 'statuses' in response and self.task_token in response['statuses']: + status = response['statuses'][self.task_token] + + if status and 'status_url' in status: + self.status_url = status['status_url'] + + return status diff --git a/qencode/tus_uploader.py b/qencode/tus_uploader.py index 23ca32d..85e222c 100644 --- a/qencode/tus_uploader.py +++ b/qencode/tus_uploader.py @@ -1,12 +1,12 @@ -import sys from tusclient import client from utils import get_tus_from_url + class UploadStatus(object): - def __init__(self, error = None, url= None, status= None): - self.url = url - self.error = error - self.status = status + def __init__(self, error=None, url=None, status=None): + self.url = url + self.error = error + self.status = status def upload(file_path=None, url=None, chunk_size=None, log_func=None): @@ -22,11 +22,13 @@ def upload(file_path=None, url=None, chunk_size=None, log_func=None): """ try: my_client = client.TusClient(url=url) - uploader = my_client.uploader(file_path=file_path, chunk_size=chunk_size, log_func=log_func) + uploader = my_client.uploader( + file_path=file_path, chunk_size=chunk_size, log_func=log_func + ) uploader.upload() url_storage = uploader.url tus_url = get_tus_from_url(url_storage) return UploadStatus(url=tus_url, status='Ok', error='') - except: - print('Error uploading file to ' + url) - raise \ No newline at end of file + except Exception: + print('Error uploading file to ' + url) + raise diff --git a/qencode/utils.py b/qencode/utils.py index 41db2f8..8395ec1 100644 --- a/qencode/utils.py +++ b/qencode/utils.py @@ -1,81 +1,99 @@ -import sys -import logging import json +import logging +import sys + +from ._compat import string_types + def is_number(s): - try: - float(s) - return True - except: - return False + try: + float(s) + return True + except Exception: + return False + def get_percent(p): - if is_number(p): - return round(p) - return 0 + if is_number(p): + return round(p) + return 0 + def is_json(value): - try: - json.loads(value) - except ValueError: - return False - return True + try: + json.loads(value) + except ValueError: + return False + return True + def rm_attributes_if_null(class_obj): - for key, val in class_obj.__dict__.items(): - if not val: - class_obj.__dict__.pop(key) + attrs = list(key for key in class_obj.__dict__.keys() if not key.startswith('__')) + for attr in attrs: + if getattr(class_obj, attr, None) is None: + delattr(class_obj, attr) + def rm_key_if_null(obj): - if isinstance(obj, dict): - return _rm_key(obj) - elif isinstance(obj, basestring): - res = _rm_key(json.loads(obj)) - return json.dumps(res) + if isinstance(obj, dict): + return _rm_key(obj) + elif isinstance(obj, string_types): + res = _rm_key(json.loads(obj)) + return json.dumps(res) + def _rm_key(_dict): - for key, val in _dict.items(): - if not val: - _dict.pop(key) - return _dict + for key, val in _dict.items(): + if not val: + _dict.pop(key) + return _dict + def progress_bar(self, custom_message=None): - message = custom_message if custom_message else '' - while 1: - barLength, status = 20, "" - progress = float(self.percent) / 100.0 - if progress >= 1.: - progress, status = 1, "\r\n" - block = int(round(barLength * progress)) - text = "\r{} [{}] {:.0f}% {}".format(message, - "#" * block + "-" * (barLength - block), round(progress * 100, 0), status) - sys.stdout.write(text) - sys.stdout.flush() - if self.task_completed: - break + message = custom_message if custom_message else '' + while 1: + barLength, status = 20, "" + progress = float(self.percent) / 100.0 + if progress >= 1.0: + progress, status = 1, "\r\n" + block = int(round(barLength * progress)) + text = "\r{} [{}] {:.0f}% {}".format( + message, + "#" * block + "-" * (barLength - block), + round(progress * 100, 0), + status, + ) + sys.stdout.write(text) + sys.stdout.flush() + if self.task_completed: + break + def log(self, path=None, name=None, log_format=None): - format = '[%(asctime)s] %(levelname)s %(message)s' if not log_format else log_format - name = name if name else '{0}.log'.format(self.task.token) - path = path if path else '' - log_name = '{0}{1}'.format(path, name) - logging.basicConfig(filename=log_name, format=format()) - logging.getLogger().setLevel(logging.INFO) - log = logging.getLogger() - while 1: - log.info('{0} | {1} | {2}'.format(self.status, self.percent, self.message)) - if self.task_completed: - break + format = ( + '[%(asctime)s] %(levelname)s %(message)s' if not log_format else log_format + ) + name = name if name else '{0}.log'.format(self.task.token) + path = path if path else '' + log_name = '{0}{1}'.format(path, name) + logging.basicConfig(filename=log_name, format=format()) + logging.getLogger().setLevel(logging.INFO) + log = logging.getLogger() + while 1: + log.info('{0} | {1} | {2}'.format(self.status, self.percent, self.message)) + if self.task_completed: + break + def get_tus_from_url(url=''): - try: - if url.find('tus:') == 0: - return url - else: - x = url.split('/')[-1] - if x == url: - return url + try: + if url.find('tus:') == 0: + return url else: - return 'tus:' + x - except: - return url \ No newline at end of file + x = url.split('/')[-1] + if x == url: + return url + else: + return 'tus:' + x + except Exception: + return url diff --git a/sample-code/metadata.py b/sample-code/metadata.py index e1b7246..5f46b75 100644 --- a/sample-code/metadata.py +++ b/sample-code/metadata.py @@ -1,13 +1,16 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +import os import sys -import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) + import qencode from qencode import QencodeClientException -#replace with your API KEY (can be found in your Project settings on Qencode portal) +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' VIDEO_URL = 'https://nyc3.s3.qencode.com/qencode/bbb_30s.mp4' @@ -15,10 +18,7 @@ if client.error: raise QencodeClientException(client.message) -print 'The client created. Expire date: %s' % client.expire +print('The client created. Expire date: %s' % client.expire) metadata = client.get_metadata(VIDEO_URL) print('Metadata: ' + metadata) - - - diff --git a/sample-code/start_custom_hls.py b/sample-code/start_custom_hls.py index 3d9728d..af575f4 100644 --- a/sample-code/start_custom_hls.py +++ b/sample-code/start_custom_hls.py @@ -1,25 +1,28 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -import sys +import json import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode +import sys import time -import json + +import qencode from qencode import QencodeClientException, QencodeTaskException -#replace with your API KEY (can be found in your Project settings on Qencode portal) +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' -params = qencode.custom_params() +params = qencode.CustomTranscodingParams() -FORMAT = qencode.format() -STREAM = qencode.stream() -DESTINATION = qencode.destination() +FORMAT = qencode.Format() +STREAM = qencode.Stream() +DESTINATION = qencode.Destination() -# set your S3 access credentials here -# for more storage types see Destination object description: https://docs.qencode.com/#010_050 +# Set your S3 access credentials here. For more storage types see Destination object +# description: https://docs.qencode.com/#010_050 DESTINATION.url = "s3://s3-eu-west-2.amazonaws.com/qencode-test" DESTINATION.key = "your-s3-key" DESTINATION.secret = "your-s3-secret" @@ -35,47 +38,46 @@ FORMAT.output = "advanced_hls" FORMAT.destination = DESTINATION -#replace with a link to your input video +# replace with a link to your input video params.source = 'https://qencode.com/static/1.mp4' params.format = [FORMAT] def start_encode(): - - """ + """ Create client object :param api_key: string. required :param api_url: string. not required :param api_version: int. not required. default 'v1' :return: task object - """ + """ - client = qencode.client(API_KEY) - if client.error: - raise QencodeClientException(client.message) + client = qencode.Client(api_key=API_KEY) + if client.error: + raise QencodeClientException(client.message) - print 'The client created. Expire date: %s' % client.expire + print('The client created. Expire date: {}'.format(client.expire)) - task = client.create_task() + task = client.create_task() - if task.error: - raise QencodeTaskException(task.message) + if task.error: + raise QencodeTaskException(task.message) - task.custom_start(params) + task.custom_start(params) - if task.error: - raise QencodeTaskException(task.message) + if task.error: + raise QencodeTaskException(task.message) - print 'Start encode. Task: %s' % task.task_token + print('Start encode. Task: %s' % task.task_token) - while True: - status = task.status() - # print status - print json.dumps(status, indent=2, sort_keys=True) - if status['error'] or status['status'] == 'completed': - break - time.sleep(5) + while True: + status = task.status() + # print status + print(json.dumps(status, indent=2, sort_keys=True)) + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) if __name__ == '__main__': - start_encode() + start_encode() diff --git a/sample-code/start_custom_mp4.py b/sample-code/start_custom_mp4.py index 9541e4c..9c699c8 100644 --- a/sample-code/start_custom_mp4.py +++ b/sample-code/start_custom_mp4.py @@ -1,24 +1,28 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import sys +import json import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode +import sys import time -import json + +import qencode from qencode import QencodeClientException, QencodeTaskException -#replace with your API KEY (can be found in your Project settings on Qencode portal) +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' -params = qencode.custom_params() +params = qencode.CustomTranscodingParams() -FORMAT = qencode.format() -DESTINATION = qencode.destination() +FORMAT = qencode.Format() +DESTINATION = qencode.Destination() -# set your S3 access credentials here -# for more storage types see Destination object description: https://docs.qencode.com/#010_050 +# set your S3 access credentials here for more storage types see Destination object +# description: https://docs.qencode.com/#010_050 DESTINATION.url = "s3://s3-eu-west-2.amazonaws.com/qencode-test" DESTINATION.key = "your-s3-key" DESTINATION.secret = "your-s3-secret" @@ -28,47 +32,46 @@ FORMAT.output = "mp4" FORMAT.destination = DESTINATION -#replace with a link to your input video +# replace with a link to your input video params.source = 'https://qencode.com/static/1.mp4' params.format = [FORMAT] - def start_encode(): - - """ + """ Create client object :param api_key: string. required :param api_url: string. not required :param api_version: int. not required. default 'v1' :return: task object - """ + """ + + client = qencode.Client(api_key=API_KEY) + if client.error: + raise QencodeClientException(client.message) - client = qencode.client(API_KEY) - if client.error: - raise QencodeClientException(client.message) + print('The client created. Expire date: %s' % client.expire) - print 'The client created. Expire date: %s' % client.expire + task = client.create_task() - task = client.create_task() + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + task.custom_start(params) - task.custom_start(params) + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + print('Start encode. Task: %s' % task.task_token) - print 'Start encode. Task: %s' % task.task_token + while True: + status = task.status() + # print status + print(json.dumps(status, indent=2, sort_keys=True)) + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) - while True: - status = task.status() - # print status - print json.dumps(status, indent=2, sort_keys=True) - if status['error'] or status['status'] == 'completed': - break - time.sleep(5) if __name__ == '__main__': - start_encode() + start_encode() diff --git a/sample-code/start_custom_tus_upload.py b/sample-code/start_custom_tus_upload.py index cf97b82..f8a7efe 100644 --- a/sample-code/start_custom_tus_upload.py +++ b/sample-code/start_custom_tus_upload.py @@ -1,15 +1,18 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +import json +import os import sys -import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode import time -import json + +import qencode from qencode import QencodeClientException, QencodeTaskException, tus_uploader -#replace with your API KEY (can be found in your Project settings on Qencode portal) +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' file_path = '/path/to/file/for/upload' @@ -31,7 +34,7 @@ def start_encode(): - """ + """ Create client object :param api_key: string. required :param api_url: string. not required @@ -39,41 +42,45 @@ def start_encode(): :return: task object """ - client = qencode.client(API_KEY) - if client.error: - raise QencodeClientException(client.message) + client = qencode.client(API_KEY) + if client.error: + raise QencodeClientException(client.message) - print 'The client created. Expire date: %s' % client.expire + print('The client created. Expire date: %s' % client.expire) - task = client.create_task() + task = client.create_task() - if task.error: - raise QencodeTaskException(task.message) + if task.error: + raise QencodeTaskException(task.message) - #get upload url from endpoint returned with /v1/create_task and task_token value - uploadUrl = task.upload_url + '/' + task.task_token + # get upload url from endpoint returned with /v1/create_task and task_token value + uploadUrl = task.upload_url + '/' + task.task_token - #do upload and get uploaded file URI - uploadedFile = tus_uploader.upload(file_path=file_path, url=uploadUrl, log_func=log_upload, chunk_size=2000000) + # do upload and get uploaded file URI + uploadedFile = tus_uploader.upload( + file_path=file_path, url=uploadUrl, log_func=log_upload, chunk_size=2000000 + ) - params = query % uploadedFile.url - task.custom_start(params) + params = query % uploadedFile.url + task.custom_start(params) - if task.error: - raise QencodeTaskException(task.message) + if task.error: + raise QencodeTaskException(task.message) - print 'Start encode. Task: %s' % task.task_token + print('Start encode. Task: %s' % task.task_token) + + while True: + status = task.status() + # print status + print(json.dumps(status, indent=2, sort_keys=True)) + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) - while True: - status = task.status() - # print status - print json.dumps(status, indent=2, sort_keys=True) - if status['error'] or status['status'] == 'completed': - break - time.sleep(5) def log_upload(msg): - print(msg) + print(msg) + if __name__ == '__main__': - start_encode() + start_encode() diff --git a/sample-code/start_custom_with_dict.py b/sample-code/start_custom_with_dict.py index 32601c3..89662ce 100644 --- a/sample-code/start_custom_with_dict.py +++ b/sample-code/start_custom_with_dict.py @@ -1,78 +1,71 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import sys +import json import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode +import sys import time -import json + +import qencode from qencode import QencodeClientException, QencodeTaskException +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + -#replace with your API KEY (can be found in your Project settings on Qencode portal) +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' -#replace with a link to your input video +# replace with a link to your input video source_url = "https://qencode.com/static/1.mp4" -format_240 = dict( - output="mp4", - size="320x240", - video_codec="libx264" -) +format_240 = dict(output="mp4", size="320x240", video_codec="libx264") -format_720 = dict( - output="mp4", - size="1280x720", - video_codec="libx264" -) +format_720 = dict(output="mp4", size="1280x720", video_codec="libx264") format = [format_240, format_720] -query = dict( - source=source_url, - format=format -) +query = dict(source=source_url, format=format) params = dict(query=query) def start_encode(): - - """ + """ Create client object :param api_key: string. required :param api_url: string. not required :param api_version: int. not required. default 'v1' :return: task object - """ + """ + + client = qencode.Client(api_key=API_KEY) + if client.error: + raise QencodeClientException(client.message) - client = qencode.client(API_KEY) - if client.error: - raise QencodeClientException(client.message) + print('The client created. Expire date: {}'.format(client.expire)) - print 'The client created. Expire date: %s' % client.expire + task = client.create_task() - task = client.create_task() + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + task.custom_start(params) - task.custom_start(params) + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + print('Start encode. Task: {}'.format(task.task_token)) - print 'Start encode. Task: %s' % task.task_token + while True: + status = task.status() + # print status + print(json.dumps(status, indent=2, sort_keys=True)) + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) - while True: - status = task.status() - # print status - print json.dumps(status, indent=2, sort_keys=True) - if status['error'] or status['status'] == 'completed': - break - time.sleep(5) if __name__ == '__main__': - start_encode() + start_encode() diff --git a/sample-code/start_custom_with_file.py b/sample-code/start_custom_with_file.py index 1133904..8bb3877 100644 --- a/sample-code/start_custom_with_file.py +++ b/sample-code/start_custom_with_file.py @@ -1,66 +1,75 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import sys +import json import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode +import sys import time -import json + +import qencode from qencode import QencodeClientException, QencodeTaskException -#replace with your API KEY (can be found in your Project settings on Qencode portal) +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' + def get_query_template(): - try: - with open(os.path.abspath(os.path.join(os.path.dirname(__file__), 'query.json'))) as data: - try: - file_data = json.load(data) - return json.dumps(file_data) - except ValueError as e: - print e - except IOError as e: - print e + try: + with open( + os.path.abspath(os.path.join(os.path.dirname(__file__), 'query.json')) + ) as data: + try: + file_data = json.load(data) + return json.dumps(file_data) + except ValueError as e: + print(e) + except IOError as e: + print(e) + params = get_query_template() def start_encode(): - """ + """ Create client object :param api_key: string. required :param api_url: string. not required :param api_version: int. not required. default 'v1' :return: task object - """ + """ + + client = qencode.Client(api_key=API_KEY) + if client.error: + raise QencodeClientException(client.message) - client = qencode.client(API_KEY) - if client.error: - raise QencodeClientException(client.message) + print('The client created. Expire date: %s' % client.expire) - print 'The client created. Expire date: %s' % client.expire + task = client.create_task() - task = client.create_task() + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + task.custom_start(params) - task.custom_start(params) + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + print('Start encode. Task: %s' % task.task_token) - print 'Start encode. Task: %s' % task.task_token + while True: + status = task.status() + # print status + print(json.dumps(status, indent=2, sort_keys=True)) + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) - while True: - status = task.status() - # print status - print json.dumps(status, indent=2, sort_keys=True) - if status['error'] or status['status'] == 'completed': - break - time.sleep(5) if __name__ == '__main__': - start_encode() + start_encode() diff --git a/sample-code/start_custom_with_json.py b/sample-code/start_custom_with_json.py index 11d8f01..c4c5368 100644 --- a/sample-code/start_custom_with_json.py +++ b/sample-code/start_custom_with_json.py @@ -1,15 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import sys +import json import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode +import sys import time -import json + +import qencode from qencode import QencodeClientException, QencodeTaskException -#replace with your API KEY (can be found in your Project settings on Qencode portal) +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' params = """ @@ -28,40 +32,40 @@ def start_encode(): - - """ + """ Create client object :param api_key: string. required :param api_url: string. not required :param api_version: int. not required. default 'v1' :return: task object - """ + """ + + client = qencode.Client(api_key=API_KEY) + if client.error: + raise QencodeClientException(client.message) - client = qencode.client(API_KEY) - if client.error: - raise QencodeClientException(client.message) + print('The client created. Expire date: %s' % client.expire) - print 'The client created. Expire date: %s' % client.expire + task = client.create_task() - task = client.create_task() + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + task.custom_start(params) - task.custom_start(params) + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + print('Start encode. Task: %s' % task.task_token) - print 'Start encode. Task: %s' % task.task_token + while True: + status = task.status() + # print status + print(json.dumps(status, indent=2, sort_keys=True)) + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) - while True: - status = task.status() - # print status - print json.dumps(status, indent=2, sort_keys=True) - if status['error'] or status['status'] == 'completed': - break - time.sleep(5) if __name__ == '__main__': - start_encode() + start_encode() diff --git a/sample-code/start_encode.py b/sample-code/start_encode.py index 2587750..fc1b2c8 100644 --- a/sample-code/start_encode.py +++ b/sample-code/start_encode.py @@ -1,29 +1,34 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import sys +import json import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) -import qencode +import sys import time -import json + +import qencode from qencode import QencodeClientException, QencodeTaskException +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + -#replace with your API KEY (can be found in your Project settings on Qencode portal) +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' -#replace with your Transcoding Profile ID (can be found in your Project settings on Qencode portal) +# replace with your Transcoding Profile ID (can be found in your Project settings on +# Qencode portal) TRANSCODING_PROFILEID = 'your-qencode-profile-id' -#replace with a link to your input video +# replace with a link to your input video VIDEO_URL = 'https://qencode.com/static/1.mp4' # or stitch -#STITCH = ["https://qencode.com/static/1.mp4", 'https://qencode.com/static/timer.mp4'] +# STITCH = ["https://qencode.com/static/1.mp4", 'https://qencode.com/static/timer.mp4'] def start_encode(): - """ + """ Create client object :param api_key: string. required :param api_url: string. not required @@ -31,35 +36,36 @@ def start_encode(): :return: task object """ - client = qencode.client(API_KEY) - if client.error: - raise QencodeClientException(client.message) + client = qencode.Client(api_key=API_KEY) + if client.error: + raise QencodeClientException(client.message) + + print('The client created. Expire date: %s' % client.expire) - print 'The client created. Expire date: %s' % client.expire + task = client.create_task() + task.start_time = 0.0 + task.duration = 10.0 - task = client.create_task() - task.start_time = 0.0 - task.duration = 10.0 + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + task.start(TRANSCODING_PROFILEID, VIDEO_URL) + # or stitch + # task.start(TRANSCODING_PROFILEID, STITCH) - task.start(TRANSCODING_PROFILEID, VIDEO_URL) - #or stitch - #task.start(TRANSCODING_PROFILEID, STITCH) + if task.error: + raise QencodeTaskException(task.message) - if task.error: - raise QencodeTaskException(task.message) + print('Start encode. Task: %s' % task.task_token) - print 'Start encode. Task: %s' % task.task_token + while True: + status = task.status() + print(json.dumps(status, indent=2, sort_keys=True)) + # print status + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) - while True: - status = task.status() - print json.dumps(status, indent=2, sort_keys=True) - # print status - if status['error'] or status['status'] == 'completed': - break - time.sleep(5) if __name__ == '__main__': - start_encode() \ No newline at end of file + start_encode() diff --git a/sample-code/start_with_callback.py b/sample-code/start_with_callback.py index 90186ca..9eada39 100644 --- a/sample-code/start_with_callback.py +++ b/sample-code/start_with_callback.py @@ -1,68 +1,72 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import sys +import json import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) +import sys + import qencode -import time -import json from qencode import QencodeClientException, QencodeTaskException +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +) + -#replace with your API KEY (can be found in your Project settings on Qencode portal) +# replace with your API KEY (can be found in your Project settings on Qencode portal) API_KEY = 'your-api-qencode-key' -#replace with your Transcoding Profile ID (can be found in your Project settings on Qencode portal) +# replace with your Transcoding Profile ID (can be found in your Project settings on +# Qencode portal) TRANSCODING_PROFILEID = 'your-qencode-profile-id' -#replace with a link to your input video +# replace with a link to your input video VIDEO_URL = 'https://qencode.com/static/1.mp4' def progress_changed_handler(status): - if status['status'] != 'completed': - print json.dumps(status, indent=2, sort_keys=True) + if status['status'] != 'completed': + print(json.dumps(status, indent=2, sort_keys=True)) def task_completed_handler(status, task_token): - print 'Completed task: %s' % task_token - print json.dumps(status, indent=2, sort_keys=True) + print('Completed task: %s' % task_token) + print(json.dumps(status, indent=2, sort_keys=True)) def start_encode(): - """ + """ Create client object :param api_key: string. required :param api_url: string. not required :param api_version: int. not required. default 'v1' :return: task object - """ + """ - client = qencode.client(API_KEY) - if client.error: - raise QencodeClientException(client.message) + client = qencode.Client(api_key=API_KEY) + if client.error: + raise QencodeClientException(client.message) - print 'The client created. Expire date: %s' % client.expire + print('The client created. Expire date: %s' % client.expire) - task = client.create_task() - task.start_time = 0.0 - task.duration = 10.0 + task = client.create_task() + task.start_time = 0.0 + task.duration = 10.0 - if task.error: - raise QencodeTaskException(task.message) + if task.error: + raise QencodeTaskException(task.message) - task.start(TRANSCODING_PROFILEID, VIDEO_URL) + task.start(TRANSCODING_PROFILEID, VIDEO_URL) - if task.error: - raise QencodeTaskException(task.message) + if task.error: + raise QencodeTaskException(task.message) - print 'Start encode. Task: %s' % task.task_token + print('Start encode. Task: %s' % task.task_token) - # using callback methods - task.progress_changed(progress_changed_handler) - task.task_completed(task_completed_handler, task.task_token) + # using callback methods + task.progress_changed(progress_changed_handler) + task.task_completed(task_completed_handler, task.task_token) if __name__ == '__main__': - start_encode() \ No newline at end of file + start_encode() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..45e1226 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,53 @@ +[flake8] +exclude = .*/,.tox,*.egg,build/,qencode/_compat.py,qencode/__*__.py, +select = E,W,F,N +max-line-length = 88 +# Stuff we ignore thanks to black: https://github.com/ambv/black/issues/429 +ignore = E111, + E121, + E122, + E123, + E124, + E125, + E126, + E201, + E202, + E203, + E221, + E222, + E225, + E226, + E227, + E231, + E241, + E251, + E261, + E262, + E265, + E271, + E272, + E302, + E303, + E306, + E502, + E701, + E702, + E703, + E704, + W291, + W292, + W293, + W391, + W503 + +[isort] +combine_as_imports= true +default_section = THIRDPARTY +include_trailing_comma = true +multi_line_output = 3 +known_pytest = pytest,py +known_first_party = qencode +split_before_closing_bracket = true +sections = FUTURE,STDLIB,PYTEST,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +line_length = 88 +not_skip = __init__.py diff --git a/setup.py b/setup.py index 144616d..3afdef0 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,20 @@ -"""A setuptools based setup module. -""" +"""A setuptools based setup module.""" +from os import path from setuptools import setup -from os import path here = path.abspath(path.dirname(__file__)) -with open('LONG_DESCRIPTION.md') as f: +with open('README.md') as f: long_description = f.read() setup( name='qencode', version='1.0', - description="Client library for main features and functionality of Qencode for Python v2.x.", + description="Python bindings for the Qencode API", long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/qencode-dev/qencode-api-python-client', - # url=here, author='Qencode Developer', author_email='team@qencode.com', license='proprietary', @@ -25,9 +23,11 @@ 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: Other/Proprietary License', - 'Programming Language :: Python :: 2.7' - + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], keywords='qencode, qencode.com, cloud.qencode.com', - packages=['qencode'] + packages=['qencode'], ) diff --git a/tests/test_client.py b/tests/test_client.py index 3d92c88..6e93742 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,16 +1,5 @@ -import unittest +import qencode -class TestQencodeApiClient(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_create_task(self): - pass - -if __name__ == '__main__': - unittest.main() \ No newline at end of file +def test_client(): + assert qencode