From 56e53d27390af864681823373fee5d6902ecd3df Mon Sep 17 00:00:00 2001 From: vladimir Date: Mon, 13 Jul 2020 15:40:16 +0300 Subject: [PATCH 01/47] getting metadata --- qencode/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index 7ca591a..ae6fd49 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -29,7 +29,7 @@ def x265_video_codec(): from exeptions import QencodeClientException, QencodeTaskException -__version__ = "0.9.29" +__version__ = "0.9.30" __status__ = "Beta" __author__ = "Qencode" diff --git a/setup.py b/setup.py index a9b6d91..a7b8da7 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='qencode', - version='0.9.29', + version='0.9.30', description="Client library for main features and functionality of Qencode for Python v2.x.", long_description=long_description, long_description_content_type='text/markdown', From ea8bad01a3aa686b900e3956e2519708c35fa392 Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Thu, 23 Jul 2020 14:03:42 +0300 Subject: [PATCH 02/47] tus uploads integrated --- qencode/tus_uploader.py | 32 +++++++++++ qencode/utils.py | 15 ++++- requirements.txt | 3 +- sample-code/start_custom_tus_upload.py | 79 ++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 qencode/tus_uploader.py create mode 100644 sample-code/start_custom_tus_upload.py diff --git a/qencode/tus_uploader.py b/qencode/tus_uploader.py new file mode 100644 index 0000000..23ca32d --- /dev/null +++ b/qencode/tus_uploader.py @@ -0,0 +1,32 @@ +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 upload(file_path=None, url=None, chunk_size=None, log_func=None): + """ + Returns upload status and url using tus protocol + + :fileUrl: + + Url address where to upload the file + + :Args: + see tusclient.uploader.Uploader for required and optional arguments. + """ + try: + my_client = client.TusClient(url=url) + 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 diff --git a/qencode/utils.py b/qencode/utils.py index 6362f96..41db2f8 100644 --- a/qencode/utils.py +++ b/qencode/utils.py @@ -65,4 +65,17 @@ def log(self, path=None, name=None, log_format=None): while 1: log.info('{0} | {1} | {2}'.format(self.status, self.percent, self.message)) if self.task_completed: - break \ No newline at end of file + 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 + else: + return 'tus:' + x + except: + return url \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2c3c5e5..6a4de85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ json urllib urllib2 -urlparse \ No newline at end of file +urlparse +tuspy \ No newline at end of file diff --git a/sample-code/start_custom_tus_upload.py b/sample-code/start_custom_tus_upload.py new file mode 100644 index 0000000..cf97b82 --- /dev/null +++ b/sample-code/start_custom_tus_upload.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +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 +from qencode import QencodeClientException, QencodeTaskException, tus_uploader + +#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' + +query = """ +{"query": { + "source": "%s", + "format": [ + { + "output": "mp4", + "size": "320x240", + "video_codec": "libx264" + } + ] + } +} +""" + + +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) + + print 'The client created. Expire date: %s' % client.expire + + task = client.create_task() + + 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 + + #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) + + if task.error: + raise QencodeTaskException(task.message) + + 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) + +def log_upload(msg): + print(msg) + +if __name__ == '__main__': + start_encode() From eca17306c8935c2fec0c4aacf43b7aada51b2096 Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Thu, 23 Jul 2020 14:26:39 +0300 Subject: [PATCH 03/47] version updated --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a7b8da7..144616d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='qencode', - version='0.9.30', + version='1.0', description="Client library for main features and functionality of Qencode for Python v2.x.", long_description=long_description, long_description_content_type='text/markdown', @@ -21,7 +21,7 @@ author_email='team@qencode.com', license='proprietary', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: Other/Proprietary License', From d88c3ad1032dca016c873b85986b77df731593bb Mon Sep 17 00:00:00 2001 From: Qencode Dev Date: Thu, 23 Jul 2020 14:27:27 +0300 Subject: [PATCH 04/47] version updated --- qencode/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index ae6fd49..7b49092 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -29,8 +29,8 @@ def x265_video_codec(): from exeptions import QencodeClientException, QencodeTaskException -__version__ = "0.9.30" -__status__ = "Beta" +__version__ = "1.0" +__status__ = "Production/Stable" __author__ = "Qencode" From 66c46749d88acab55f28e9713ebb046726578831 Mon Sep 17 00:00:00 2001 From: vladimir Date: Thu, 23 Jul 2020 14:31:57 +0300 Subject: [PATCH 05/47] added long description --- .gitignore | 3 +-- LONG_DESCRIPTION.md | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 LONG_DESCRIPTION.md diff --git a/.gitignore b/.gitignore index ad3d1b1..5f8c625 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,4 @@ .cache/ .vscode/ -.idea/ -LONG_DESCRIPTION.md \ No newline at end of file +.idea/ \ No newline at end of file diff --git a/LONG_DESCRIPTION.md b/LONG_DESCRIPTION.md new file mode 100644 index 0000000..f9eb93b --- /dev/null +++ b/LONG_DESCRIPTION.md @@ -0,0 +1,40 @@ +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. + +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 + +Transfer & Storage Options + * S3 Qencode + * AWS + * Google Cloud + * Backblaze + * Azure + * FTP + * HTTP(S) + * VPN From 09642faf4cba2f8ef5113afe4a5a892c5e2aa572 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:38:34 -0500 Subject: [PATCH 06/47] gitignore: .vim/ --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5f8c625..86aedcc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ .cache/ .vscode/ -.idea/ \ No newline at end of file +.idea/ +.vim/ +LONG_DESCRIPTION.md From a4140e6ad2548573796329ad6986bbaff0447619 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:03:45 -0500 Subject: [PATCH 07/47] gitignore: Add Python's default gitignore See also: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore --- .gitignore | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 86aedcc..76cc8c3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,153 @@ *.log *.sql -*.pyc .cache/ .vscode/ .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/ From 2dcc9258ebb55109715ec9b5ef4858fcc99ec233 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:57:33 -0500 Subject: [PATCH 08/47] gitignore: Ignore pip-wheel-metadata/ --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 76cc8c3..de9133d 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,6 @@ dmypy.json # Cython debug symbols cython_debug/ + +# Pip +pip-wheel-metadata/ From 0758f2816a09e19cc297f919aa23bce6f82c1446 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 17:59:05 -0500 Subject: [PATCH 09/47] Add pyproject.toml for Poetry See also: https://python-poetry.org/ --- pyproject.toml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..85d1844 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "qencode-api-python-client" +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] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" From 1b13a021368a781ac1898b91f669fce3f53569cb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:03:57 -0500 Subject: [PATCH 10/47] pyproject.toml: Add test and doc dependencies for Python 2 and 3 --- pyproject.toml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 85d1844..9dedbe0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,30 @@ license = "MIT" python = "~2.7 || ^3.5" [tool.poetry.dev-dependencies] +black = {version="==19.10b0", python="^3.6"} +docutils = "*" +flake8 = "*" +isort = "*" +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"] From cbf876189a9dcff3de1e394032aad8bfb870dd76 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:04:32 -0500 Subject: [PATCH 11/47] Add poetry lockfile --- poetry.lock | 1426 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1426 insertions(+) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..fac186c --- /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\"" +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\" 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 = "4f94a72bb63e400747a6fa10d6f328d292bca2588cd7f3fed64aff1756261d04" +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"}, +] From 2057f0d3209d2af13e4a64c483be66a80db0fb88 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:05:04 -0500 Subject: [PATCH 12/47] Add Makefile for tests, flake8 linting, formatting --- Makefile | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Makefile 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 From 927560fe998ebea479ee75eb06419643d26e98db Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:08:30 -0500 Subject: [PATCH 13/47] Add dependabot workflow --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml 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 From f81499515dab6d34a6c8b38afd587476dfeda82a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:13:22 -0500 Subject: [PATCH 14/47] Add github workflow --- .github/workflows/qencode-ci.yml | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/qencode-ci.yml diff --git a/.github/workflows/qencode-ci.yml b/.github/workflows/qencode-ci.yml new file mode 100644 index 0000000..6e41a69 --- /dev/null +++ b/.github/workflows/qencode-ci.yml @@ -0,0 +1,34 @@ +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 + 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 + with: + token: ${{ secrets.CODECOV_TOKEN }} From 4e5c76be43b7e42c9694d512de65bad35b3607f2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:24:39 -0500 Subject: [PATCH 15/47] Add flake8 and isort config --- setup.cfg | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4ab22d5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,53 @@ +[flake8] +exclude = .*/,.tox,*.egg,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 From cac4e2cd77be905b286ce5c9149edda612857a78 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:25:05 -0500 Subject: [PATCH 16/47] black: skip string normalization --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9dedbe0..9c5400e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[tool.black] +skip-string-normalization = true + [tool.poetry] name = "qencode-api-python-client" version = "1.0.0" From 4a136edb2c7f8e236bc3bad2cc6d7ca68631f5ce Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:26:03 -0500 Subject: [PATCH 17/47] Run: make black isort --- qencode/__init__.py | 42 +-- qencode/client.py | 88 +++---- qencode/const.py | 26 +- qencode/custom_params.py | 233 +++++++++-------- qencode/exeptions.py | 21 +- qencode/httptools.py | 78 +++--- qencode/metadata.py | 15 +- qencode/task.py | 343 +++++++++++++------------ qencode/tus_uploader.py | 18 +- qencode/utils.py | 137 +++++----- sample-code/metadata.py | 15 +- sample-code/start_custom_hls.py | 56 ++-- sample-code/start_custom_mp4.py | 58 +++-- sample-code/start_custom_tus_upload.py | 70 ++--- sample-code/start_custom_with_dict.py | 74 +++--- sample-code/start_custom_with_file.py | 75 +++--- sample-code/start_custom_with_json.py | 55 ++-- sample-code/start_encode.py | 69 ++--- sample-code/start_with_callback.py | 62 ++--- setup.py | 8 +- tests/test_client.py | 8 +- 21 files changed, 815 insertions(+), 736 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index 7b49092..f0636b4 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -1,37 +1,47 @@ - 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() + from custom_params import CustomTranscodingParams + + return CustomTranscodingParams() + def format(): - from custom_params import Format - return Format() + from custom_params import Format + + return Format() + def destination(): - from custom_params import Destination - return Destination() + from custom_params import Destination + + return Destination() + def stream(): - from custom_params import Stream - return Stream() + from custom_params import Stream + + return Stream() + def x264_video_codec(): - from custom_params import Libx264_VideoCodecParameters - return Libx264_VideoCodecParameters() + from custom_params import Libx264_VideoCodecParameters + + return Libx264_VideoCodecParameters() + def x265_video_codec(): - from custom_params import Libx265_VideoCodecParameters - return Libx265_VideoCodecParameters() + 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/client.py b/qencode/client.py index 993043c..44d5661 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -1,51 +1,51 @@ from httptools import Http -from task import Task from metadata import Metadata +from task import Task + 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 + """ + + 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..e863054 100644 --- a/qencode/custom_params.py +++ b/qencode/custom_params.py @@ -1,144 +1,153 @@ import json from json import JSONEncoder + 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) """ - 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 __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) + 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: + 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 diff --git a/qencode/exeptions.py b/qencode/exeptions.py index 36b5fdc..fa5b795 100644 --- a/qencode/exeptions.py +++ b/qencode/exeptions.py @@ -1,18 +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] + 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) + 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) - - - - + def __init__(self, message, *args): + super(QencodeTaskException, self).__init__(message, *args) diff --git a/qencode/httptools.py b/qencode/httptools.py index 5a3d89f..fe82e79 100644 --- a/qencode/httptools.py +++ b/qencode/httptools.py @@ -3,44 +3,48 @@ import urllib2 from urlparse import urljoin + 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 = 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 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..ffd2da1 100644 --- a/qencode/metadata.py +++ b/qencode/metadata.py @@ -1,17 +1,22 @@ +import urllib2 + from task import * + from qencode import QencodeTaskException -import urllib2 -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() @@ -36,5 +41,3 @@ def get(self, uri): data = urllib2.urlopen(url).read() return data - - diff --git a/qencode/task.py b/qencode/task.py index c867647..6b799dd 100644 --- a/qencode/task.py +++ b/qencode/task.py @@ -1,27 +1,29 @@ -from custom_params import Query, CustomTranscodingParams -from const import * -import time import json +import time + +from const import * +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 + 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 @@ -30,162 +32,163 @@ def start(self, profiles, video_url, **kwargs): :return: None """ - if not self.error: - # self._create_task(1) - data = self._prepare_data(profiles, video_url, **kwargs) + 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) + 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 + 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 - + 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 diff --git a/qencode/tus_uploader.py b/qencode/tus_uploader.py index 23ca32d..487158c 100644 --- a/qencode/tus_uploader.py +++ b/qencode/tus_uploader.py @@ -1,12 +1,14 @@ 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 +24,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 + print('Error uploading file to ' + url) + raise diff --git a/qencode/utils.py b/qencode/utils.py index 41db2f8..193c825 100644 --- a/qencode/utils.py +++ b/qencode/utils.py @@ -1,81 +1,96 @@ -import sys -import logging import json +import logging +import sys + def is_number(s): - try: - float(s) - return True - except: - return False + try: + float(s) + return True + except: + 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) + for key, val in class_obj.__dict__.items(): + if not val: + class_obj.__dict__.pop(key) + 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, basestring): + 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: + return url diff --git a/sample-code/metadata.py b/sample-code/metadata.py index e1b7246..a8402ea 100644 --- a/sample-code/metadata.py +++ b/sample-code/metadata.py @@ -1,13 +1,17 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import sys import os.path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) +import sys + 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' @@ -18,7 +22,4 @@ print 'The client created. Expire date: %s' % client.expire metadata = client.get_metadata(VIDEO_URL) -print('Metadata: ' + metadata) - - - +print ('Metadata: ' + metadata) diff --git a/sample-code/start_custom_hls.py b/sample-code/start_custom_hls.py index 3d9728d..a86f994 100644 --- a/sample-code/start_custom_hls.py +++ b/sample-code/start_custom_hls.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 = qencode.custom_params() @@ -35,14 +39,14 @@ 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 @@ -50,32 +54,32 @@ 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) - 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..c734b4b 100644 --- a/sample-code/start_custom_mp4.py +++ b/sample-code/start_custom_mp4.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 = qencode.custom_params() @@ -28,15 +32,14 @@ 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 @@ -44,31 +47,32 @@ 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) + 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..9f77ce0 100644 --- a/sample-code/start_custom_tus_upload.py +++ b/sample-code/start_custom_tus_upload.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, 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 +35,7 @@ def start_encode(): - """ + """ Create client object :param api_key: string. required :param api_url: string. not required @@ -39,41 +43,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..0f1c36e 100644 --- a/sample-code/start_custom_with_dict.py +++ b/sample-code/start_custom_with_dict.py @@ -1,46 +1,39 @@ #!/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 @@ -48,31 +41,32 @@ 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) + 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_file.py b/sample-code/start_custom_with_file.py index 1133904..166b72b 100644 --- a/sample-code/start_custom_with_file.py +++ b/sample-code/start_custom_with_file.py @@ -1,34 +1,42 @@ #!/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: + 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 - 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 @@ -36,31 +44,32 @@ 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) + 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..c38eb76 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 = """ @@ -29,7 +33,7 @@ def start_encode(): - """ + """ Create client object :param api_key: string. required :param api_url: string. not required @@ -37,31 +41,32 @@ 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) + 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..b6e3cc2 100644 --- a/sample-code/start_encode.py +++ b/sample-code/start_encode.py @@ -1,29 +1,33 @@ #!/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 +35,36 @@ 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.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..5916935 100644 --- a/sample-code/start_with_callback.py +++ b/sample-code/start_with_callback.py @@ -1,37 +1,41 @@ #!/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' 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 @@ -39,30 +43,30 @@ 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.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.py b/setup.py index 144616d..aad5bcd 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ """A setuptools based setup module. """ -from setuptools import setup from os import path +from setuptools import setup + here = path.abspath(path.dirname(__file__)) with open('LONG_DESCRIPTION.md') as f: @@ -25,9 +26,8 @@ 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: Other/Proprietary License', - 'Programming Language :: Python :: 2.7' - + 'Programming Language :: Python :: 2.7', ], 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..599b5b6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,15 +2,15 @@ class TestQencodeApiClient(unittest.TestCase): - def setUp(self): - pass + pass def tearDown(self): - pass + pass def test_create_task(self): pass + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From adf1d24d5a8baa08071ed315e005cbf6663ce015 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:36:31 -0500 Subject: [PATCH 18/47] Add _compat module for 2.x / 3.x compatibility --- qencode/_compat.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 qencode/_compat.py diff --git a/qencode/_compat.py b/qencode/_compat.py new file mode 100644 index 0000000..364432e --- /dev/null +++ b/qencode/_compat.py @@ -0,0 +1,10 @@ +# flake8: NOQA +import sys + +PY2 = sys.version_info[0] == 2 + + +if PY2: + string_types = (str, unicode) +else: + string_types = (str,) From 976b7546e0e2ff9eba6f20d84dc3dac26cb2db31 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:37:56 -0500 Subject: [PATCH 19/47] poetry.toml: Add .venv --- poetry.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 poetry.toml 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 From 0ce1ac353df2a2cb634dc8cecc6f5fbfdfa0085e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:38:07 -0500 Subject: [PATCH 20/47] flake8 linting fixes --- qencode/custom_params.py | 10 +++---- qencode/task.py | 65 ++++++++++++++++++++-------------------- qencode/tus_uploader.py | 4 +-- qencode/utils.py | 8 +++-- 4 files changed, 44 insertions(+), 43 deletions(-) diff --git a/qencode/custom_params.py b/qencode/custom_params.py index e863054..d900fa6 100644 --- a/qencode/custom_params.py +++ b/qencode/custom_params.py @@ -8,9 +8,9 @@ class CustomTranscodingParams(object): """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 @@ -143,11 +143,11 @@ def validate_params(self): self.error = True self.message = 'Params is required' return - if not 'source' in self.params.__dict__: + if 'source' not in self.params.__dict__: self.error = True self.message = 'Params: source is required' return - if not 'format' in self.params.__dict__: + if 'format' not in self.params.__dict__: self.error = True self.message = 'Params: format is required' return diff --git a/qencode/task.py b/qencode/task.py index 6b799dd..e77f601 100644 --- a/qencode/task.py +++ b/qencode/task.py @@ -1,9 +1,10 @@ import json import time -from const import * -from custom_params import CustomTranscodingParams, Query -from utils import is_json, rm_key_if_null +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): @@ -19,19 +20,20 @@ def __init__(self, access_token, connect, debug=False, **kwargs): self._debug = debug self.message = '' self.error = None - self.repeat = kwargs.get('repeats') if kwargs.get('repeats') else REPEAT + 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 - - """ + """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) @@ -42,11 +44,11 @@ def start(self, profiles, video_url, **kwargs): 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 - - """ + :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' @@ -75,20 +77,20 @@ def progress_changed(self, callback, *args, **kwargs): if status['error']: return callback(status, *args, **kwargs) callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: + if status.get('status') in constants.COMPLETED_STATUS: break - time.sleep(SLEEP_REGULAR) + 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 COMPLETED_STATUS: + if status.get('status') in constants.COMPLETED_STATUS: return callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: + if status.get('status') in constants.COMPLETED_STATUS: break - time.sleep(SLEEP_REGULAR) + time.sleep(constants.SLEEP_REGULAR) def _prepare_query(self, params): if isinstance(params, CustomTranscodingParams): @@ -108,20 +110,19 @@ def _prepare_query(self, params): query = rm_key_if_null(params) return json.dumps(query) - if isinstance(params, basestring): + 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 = "JSON is not well formatted: {0} Is not defined".format( - params - ) - except Exception as e: + self.message = "{}: {} Is not defined".format(error_msg, params) + except Exception: pass finally: - self.message = "JSON is not well formatted" + self.message = error_msg def _prepare_data(self, profiles, video_url, **kwargs): data = dict( @@ -157,8 +158,8 @@ def _create_task(self, count): self.message = res.get('message') if self.error and self.error == 8: - if count < REPEAT: - time.sleep(SLEEP_ERROR) + if count < constants.REPEAT: + time.sleep(constants.SLEEP_ERROR) self._create_task(count + 1) def _start_encode(self, api_name, data): @@ -174,7 +175,7 @@ def _status(self): response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) status = None - if response['error'] == ERROR_BAD_TOKENS: + 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']: diff --git a/qencode/tus_uploader.py b/qencode/tus_uploader.py index 487158c..85e222c 100644 --- a/qencode/tus_uploader.py +++ b/qencode/tus_uploader.py @@ -1,5 +1,3 @@ -import sys - from tusclient import client from utils import get_tus_from_url @@ -31,6 +29,6 @@ def upload(file_path=None, url=None, chunk_size=None, log_func=None): url_storage = uploader.url tus_url = get_tus_from_url(url_storage) return UploadStatus(url=tus_url, status='Ok', error='') - except: + except Exception: print('Error uploading file to ' + url) raise diff --git a/qencode/utils.py b/qencode/utils.py index 193c825..81aafa2 100644 --- a/qencode/utils.py +++ b/qencode/utils.py @@ -2,12 +2,14 @@ import logging import sys +from ._compat import string_types + def is_number(s): try: float(s) return True - except: + except Exception: return False @@ -34,7 +36,7 @@ def rm_attributes_if_null(class_obj): def rm_key_if_null(obj): if isinstance(obj, dict): return _rm_key(obj) - elif isinstance(obj, basestring): + elif isinstance(obj, string_types): res = _rm_key(json.loads(obj)) return json.dumps(res) @@ -92,5 +94,5 @@ def get_tus_from_url(url=''): return url else: return 'tus:' + x - except: + except Exception: return url From 8d906a153d6403c87d34aebc76ea069e39d1e142 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:43:58 -0500 Subject: [PATCH 21/47] isort: Keep <5 --- poetry.lock | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index fac186c..2208a68 100644 --- a/poetry.lock +++ b/poetry.lock @@ -161,7 +161,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytes [[package]] category = "dev" description = "Backports and enhancements for the contextlib module" -marker = "python_version < \"3\"" +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.*" @@ -1001,7 +1001,7 @@ version = "0.5.1" [[package]] category = "dev" description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3\" or python_version >= \"3\" and python_version < \"3.8\"" +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" @@ -1017,7 +1017,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] [metadata] -content-hash = "4f94a72bb63e400747a6fa10d6f328d292bca2588cd7f3fed64aff1756261d04" +content-hash = "5545dfabdca177c9c6444e7933b99c1f1055dd7aec08a139564ed6f9171028cf" python-versions = "~2.7 || ^3.5" [metadata.files] diff --git a/pyproject.toml b/pyproject.toml index 9c5400e..23d7658 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ python = "~2.7 || ^3.5" black = {version="==19.10b0", python="^3.6"} docutils = "*" flake8 = "*" -isort = "*" +isort = "<5" pytest = [ {version="<4.7.0", python="<3"}, {version="*", python=">=3"} From 7e9128023d8f4971a5003f8363d5a22fcd1a2f27 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:45:19 -0500 Subject: [PATCH 22/47] Tweaks for setup.py, linting --- setup.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index aad5bcd..9fee6eb 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,4 @@ -"""A setuptools based setup module. -""" - +"""A setuptools based setup module.""" from os import path from setuptools import setup @@ -13,11 +11,10 @@ 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', From 7bf64751452a48e3af2eb26cfaa25611dcabe754 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 10 Jul 2020 18:53:59 -0500 Subject: [PATCH 23/47] flake8: sample-code --- sample-code/metadata.py | 7 +++---- sample-code/start_custom_hls.py | 14 ++++++-------- sample-code/start_custom_mp4.py | 13 ++++++------- sample-code/start_custom_tus_upload.py | 11 +++++------ sample-code/start_custom_with_dict.py | 9 ++++----- sample-code/start_custom_with_file.py | 10 +++++----- sample-code/start_custom_with_json.py | 9 ++++----- sample-code/start_encode.py | 9 +++++---- sample-code/start_with_callback.py | 16 ++++++++-------- 9 files changed, 46 insertions(+), 52 deletions(-) diff --git a/sample-code/metadata.py b/sample-code/metadata.py index a8402ea..5f46b75 100644 --- a/sample-code/metadata.py +++ b/sample-code/metadata.py @@ -1,7 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -import os.path +import os import sys import qencode @@ -19,7 +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) +print('Metadata: ' + metadata) diff --git a/sample-code/start_custom_hls.py b/sample-code/start_custom_hls.py index a86f994..489c5dc 100644 --- a/sample-code/start_custom_hls.py +++ b/sample-code/start_custom_hls.py @@ -1,6 +1,5 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - import json import os.path import sys @@ -22,8 +21,8 @@ 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" @@ -45,20 +44,19 @@ 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) - print 'The client created. Expire date: %s' % client.expire + print('The client created. Expire date: {}'.format(client.expire)) task = client.create_task() @@ -70,12 +68,12 @@ def start_encode(): 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) + print(json.dumps(status, indent=2, sort_keys=True)) if status['error'] or status['status'] == 'completed': break time.sleep(5) diff --git a/sample-code/start_custom_mp4.py b/sample-code/start_custom_mp4.py index c734b4b..1a52f42 100644 --- a/sample-code/start_custom_mp4.py +++ b/sample-code/start_custom_mp4.py @@ -21,8 +21,8 @@ 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" @@ -38,20 +38,19 @@ 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) - print 'The client created. Expire date: %s' % client.expire + print('The client created. Expire date: %s' % client.expire) task = client.create_task() @@ -63,12 +62,12 @@ def start_encode(): 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) + print(json.dumps(status, indent=2, sort_keys=True)) if status['error'] or status['status'] == 'completed': break time.sleep(5) diff --git a/sample-code/start_custom_tus_upload.py b/sample-code/start_custom_tus_upload.py index 9f77ce0..f8a7efe 100644 --- a/sample-code/start_custom_tus_upload.py +++ b/sample-code/start_custom_tus_upload.py @@ -1,8 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - import json -import os.path +import os import sys import time @@ -47,7 +46,7 @@ def start_encode(): 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() @@ -68,19 +67,19 @@ def start_encode(): 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) + 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__': diff --git a/sample-code/start_custom_with_dict.py b/sample-code/start_custom_with_dict.py index 0f1c36e..b325597 100644 --- a/sample-code/start_custom_with_dict.py +++ b/sample-code/start_custom_with_dict.py @@ -32,20 +32,19 @@ 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) - print 'The client created. Expire date: %s' % client.expire + print('The client created. Expire date: {}'.format(client.expire)) task = client.create_task() @@ -57,12 +56,12 @@ def start_encode(): if task.error: raise QencodeTaskException(task.message) - print 'Start encode. Task: %s' % task.task_token + print('Start encode. Task: {}'.format(task.task_token)) while True: status = task.status() # print status - print json.dumps(status, indent=2, sort_keys=True) + print(json.dumps(status, indent=2, sort_keys=True)) if status['error'] or status['status'] == 'completed': break time.sleep(5) diff --git a/sample-code/start_custom_with_file.py b/sample-code/start_custom_with_file.py index 166b72b..15606d7 100644 --- a/sample-code/start_custom_with_file.py +++ b/sample-code/start_custom_with_file.py @@ -26,9 +26,9 @@ def get_query_template(): file_data = json.load(data) return json.dumps(file_data) except ValueError as e: - print e + print(e) except IOError as e: - print e + print(e) params = get_query_template() @@ -48,7 +48,7 @@ def start_encode(): 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() @@ -60,12 +60,12 @@ def start_encode(): 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) + print(json.dumps(status, indent=2, sort_keys=True)) if status['error'] or status['status'] == 'completed': break time.sleep(5) diff --git a/sample-code/start_custom_with_json.py b/sample-code/start_custom_with_json.py index c38eb76..993fe92 100644 --- a/sample-code/start_custom_with_json.py +++ b/sample-code/start_custom_with_json.py @@ -32,20 +32,19 @@ 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) - print 'The client created. Expire date: %s' % client.expire + print('The client created. Expire date: %s' % client.expire) task = client.create_task() @@ -57,12 +56,12 @@ def start_encode(): 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) + print(json.dumps(status, indent=2, sort_keys=True)) if status['error'] or status['status'] == 'completed': break time.sleep(5) diff --git a/sample-code/start_encode.py b/sample-code/start_encode.py index b6e3cc2..be0af14 100644 --- a/sample-code/start_encode.py +++ b/sample-code/start_encode.py @@ -17,7 +17,8 @@ # 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 @@ -39,7 +40,7 @@ def start_encode(): 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 @@ -55,11 +56,11 @@ def start_encode(): 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(json.dumps(status, indent=2, sort_keys=True)) # print status if status['error'] or status['status'] == 'completed': break diff --git a/sample-code/start_with_callback.py b/sample-code/start_with_callback.py index 5916935..7271e3f 100644 --- a/sample-code/start_with_callback.py +++ b/sample-code/start_with_callback.py @@ -4,7 +4,6 @@ import json import os.path import sys -import time import qencode from qencode import QencodeClientException, QencodeTaskException @@ -17,7 +16,8 @@ # 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 @@ -26,12 +26,12 @@ def progress_changed_handler(status): if status['status'] != 'completed': - print json.dumps(status, indent=2, sort_keys=True) + 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(): @@ -41,13 +41,13 @@ def start_encode(): :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) - 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 @@ -61,7 +61,7 @@ def start_encode(): 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) From 684d185926483d27d163ca03540214f3a7a4b0de Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 05:39:34 -0500 Subject: [PATCH 24/47] docs: Format with prettier --- README.md | 83 ++++++++++++++++++++++++---------------------- docs/install.md | 8 ++--- docs/quickstart.md | 15 ++++----- docs/usage.md | 17 +++++----- 4 files changed, 63 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 181fa9c..58b095f 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ ## qencode-api-python-client - **install sdk libraries from github** -```` +``` 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 -```` +``` **Usage** -```` +``` import qencode client = qencode.client(API_KEY) @@ -31,7 +31,7 @@ task.start(TRANSCODING_PROFILEID, VIDEO_URL) #getting video metadata: metadata = client.get_metadata(VIDEO_URL) -```` +``` **Documentation** @@ -44,38 +44,41 @@ Inside this library, you will find sample code for creating [video transcoding]( 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..173c83d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -2,16 +2,16 @@ **install sdk libraries from github** -```` +``` 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 +``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 5a6bdaa..cdc3445 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -2,23 +2,23 @@ **install sdk libraries from github** -```` +``` 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 -```` +``` **Usage** -```` +``` import qencode client = qencode.client(API_KEY) @@ -27,9 +27,8 @@ client.create() task = client.create_task() task.start(TRANSCODING_PROFILEID, VIDO_URL) -```` - +``` **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..faf955a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,7 +2,7 @@ **Usage by transcoding profile ID** -```` +``` import sys import os.path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) @@ -57,11 +57,11 @@ def start_encode(): if __name__ == '__main__': start_encode() -```` +``` **Usage by custom parameters** -```` +``` import sys import os.path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) @@ -144,25 +144,26 @@ def start_encode(): if __name__ == '__main__': start_encode() -```` +``` + **Usage with callback methods** -```` +``` def my_callback(e): print e def my_callback2(e): print e - + ... task.start(TRANSCODING_PROFILEID, VIDO_URL) -if task.error: +if task.error: raise SystemExit task.progress_changed(my_callback) task.task_completed(my_callback2) -```` +``` **Documentation** From b9ab3aba1ad9d8e17c88c471fe0959a45d343dc3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 05:42:21 -0500 Subject: [PATCH 25/47] docs: Don't use sudo for pip command sudo is used for global package installations, most libraries incorporate python packages via local virtual environments. Using sudo would give chmods the user couldn't access. If the user needs install globally, they can imply sudo, but even then it's not advised for python libraries intended to be used programmatically. --- README.md | 2 +- docs/install.md | 2 +- docs/quickstart.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 58b095f..3abc26e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ python setup.py install **install from pip** ``` -sudo pip install qencode +pip install --upgrade qencode ``` **Usage** diff --git a/docs/install.md b/docs/install.md index 173c83d..e90496e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -13,5 +13,5 @@ python setup.py install **install from pip** ``` -sudo pip install qencode +pip install --upgrade qencode ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index cdc3445..613384c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -13,7 +13,7 @@ python setup.py install **install from pip** ``` -sudo pip install qencode +pip install --upgrade qencode ``` **Usage** From 717f287fb86aa5200f48d7503f4c57ebe713930c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 05:59:37 -0500 Subject: [PATCH 26/47] Docs: Fix examples, indentation, formatting, development instructions --- README.md | 46 +++++++----- docs/install.md | 24 +++--- docs/quickstart.md | 32 ++++---- docs/usage.md | 178 ++++++++++++++++++++++----------------------- 4 files changed, 146 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 3abc26e..9960360 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,36 @@ -## qencode-api-python-client +# Qencode Python Library -**install sdk libraries from github** +The Qencode Python library provides convenient access to the Qencode API +for applications written in the Python language. -``` -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 -``` +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. -**install from pip** +## 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 ``` -**Usage** +## 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 +poetry shell +poetry install ``` + +## Usage + +```python import qencode client = qencode.client(API_KEY) @@ -27,19 +39,15 @@ 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: diff --git a/docs/install.md b/docs/install.md index e90496e..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** - -``` -pip install --upgrade qencode +poetry shell +poetry install ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 613384c..dca1bb4 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,34 +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 -``` - -**install from pip** - -``` -pip install --upgrade qencode +poetry shell +poetry install ``` -**Usage** +## Usage -``` +```python import qencode client = qencode.client(API_KEY) client.create() task = client.create_task() -task.start(TRANSCODING_PROFILEID, VIDO_URL) - ``` -**Documentation** +## Documentation Documentation is available at diff --git a/docs/usage.md b/docs/usage.md index faf955a..5319ecf 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,73 +1,65 @@ -##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) + 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() @@ -103,68 +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) + 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() + start_encode() ``` -**Usage with callback methods** +## Usage with callback methods ``` -def my_callback(e): - print e +def on_process_changed(process_data): + print('on_process_changed', process_data) -def my_callback2(e): - print e -... +def on_task_completed(process_data): + print('on_task_completed', process_data) -task.start(TRANSCODING_PROFILEID, VIDO_URL) + +# ... + +task.start(TRANSCODING_PROFILEID, VIDEO_URL) if task.error: - raise SystemExit + raise SystemExit task.progress_changed(my_callback) -task.task_completed(my_callback2) +task.task_completed(on_task_completed) ``` -**Documentation** +## Documentation Documentation is available at From 081aa350da042252a3346769a6df877b7203ad37 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:02:35 -0500 Subject: [PATCH 27/47] setup.py: Fix LONG_DESCRIPTION --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9fee6eb..063d62d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ here = path.abspath(path.dirname(__file__)) -with open('LONG_DESCRIPTION.md') as f: +with open('README.md') as f: long_description = f.read() setup( From e9a142b5795249ba318756f616482fafdf454e95 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:05:24 -0500 Subject: [PATCH 28/47] poetry: Use package name "qencode" --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 23d7658..ff5a125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ skip-string-normalization = true [tool.poetry] -name = "qencode-api-python-client" +name = "qencode" version = "1.0.0" description = "Python bindings for the Qencode API" authors = ["Qencode Team "] From b6b259c1879f58de1d558b1499502951bfbd4c32 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:07:30 -0500 Subject: [PATCH 29/47] Filename typo --- qencode/__init__.py | 2 +- qencode/{exeptions.py => exceptions.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename qencode/{exeptions.py => exceptions.py} (100%) diff --git a/qencode/__init__.py b/qencode/__init__.py index f0636b4..c9ededd 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -40,7 +40,7 @@ def x265_video_codec(): return Libx265_VideoCodecParameters() -from exeptions import QencodeClientException, QencodeTaskException +from exceptions import QencodeClientException, QencodeTaskException __version__ = "1.0" __status__ = "Production/Stable" diff --git a/qencode/exeptions.py b/qencode/exceptions.py similarity index 100% rename from qencode/exeptions.py rename to qencode/exceptions.py From 3752b82c4666c1e3a08d3233460afb7cbc5d1761 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:08:09 -0500 Subject: [PATCH 30/47] Grab package fix relative import (python 2/3 compatibility) --- qencode/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qencode/__init__.py b/qencode/__init__.py index c9ededd..d82f763 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -1,3 +1,6 @@ +from .exceptions import QencodeClientException, QencodeTaskException + + def client(api_key, api_url=None, version=None, **kwargs): from client import QencodeApiClient @@ -40,8 +43,6 @@ def x265_video_codec(): return Libx265_VideoCodecParameters() -from exceptions import QencodeClientException, QencodeTaskException - __version__ = "1.0" __status__ = "Production/Stable" __author__ = "Qencode" From 12235f22ad2d990294f5f7049201a0b9edf41b22 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:10:43 -0500 Subject: [PATCH 31/47] Test: use pytest style, import qencode --- tests/test_client.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 599b5b6..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() +def test_client(): + assert qencode From 2b329c84e4e28fa046427222000b11df77953b81 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:12:55 -0500 Subject: [PATCH 32/47] Import fixes, cleanup --- qencode/client.py | 6 ------ qencode/custom_params.py | 13 ++++--------- qencode/metadata.py | 2 +- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/qencode/client.py b/qencode/client.py index 44d5661..7e0d3f8 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -4,12 +4,6 @@ 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/' diff --git a/qencode/custom_params.py b/qencode/custom_params.py index d900fa6..3c5d2b9 100644 --- a/qencode/custom_params.py +++ b/qencode/custom_params.py @@ -1,15 +1,16 @@ import json from json import JSONEncoder -from utils import rm_attributes_if_null +from .utils import rm_attributes_if_null class CustomTranscodingParams(object): """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): @@ -23,12 +24,6 @@ def remove_null_params(self): class Format(object): - """ - :var - :var - - """ - def __init__(self): self.output = None self.file_extension = None diff --git a/qencode/metadata.py b/qencode/metadata.py index ffd2da1..c9d59b9 100644 --- a/qencode/metadata.py +++ b/qencode/metadata.py @@ -2,7 +2,7 @@ from task import * -from qencode import QencodeTaskException +from .exceptions import QencodeTaskException class Metadata(Task): From 23a6a24c4af2b63291601554f2f3185935c2f26f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:19:28 -0500 Subject: [PATCH 33/47] Reorganize __init__ package to fix shadowing issues, make API consistent As a helper/for code completion and simplicity, make the params available through the root module withing having to import via qencode.custom_params. --- docs/usage.md | 10 ++++---- qencode/__init__.py | 44 ++++++--------------------------- sample-code/start_custom_hls.py | 8 +++--- sample-code/start_custom_mp4.py | 6 ++--- 4 files changed, 20 insertions(+), 48 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 5319ecf..94b9eda 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -62,12 +62,12 @@ 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 = "..." diff --git a/qencode/__init__.py b/qencode/__init__.py index d82f763..a9f4070 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -1,3 +1,11 @@ +from .custom_params import ( + CustomTranscodingParams, + Destination, + Format, + Libx264_VideoCodecParameters, + Libx265_VideoCodecParameters, + Stream, +) from .exceptions import QencodeClientException, QencodeTaskException @@ -7,42 +15,6 @@ def client(api_key, api_url=None, version=None, **kwargs): 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() - - -def x265_video_codec(): - from custom_params import Libx265_VideoCodecParameters - - return Libx265_VideoCodecParameters() - - __version__ = "1.0" __status__ = "Production/Stable" __author__ = "Qencode" diff --git a/sample-code/start_custom_hls.py b/sample-code/start_custom_hls.py index 489c5dc..b481c67 100644 --- a/sample-code/start_custom_hls.py +++ b/sample-code/start_custom_hls.py @@ -15,11 +15,11 @@ # 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 diff --git a/sample-code/start_custom_mp4.py b/sample-code/start_custom_mp4.py index 1a52f42..fb97fd7 100644 --- a/sample-code/start_custom_mp4.py +++ b/sample-code/start_custom_mp4.py @@ -16,10 +16,10 @@ # 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 From 27565dfabfa890437683f5b4a1c3c15dff4a2e70 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:23:05 -0500 Subject: [PATCH 34/47] Rename QencodeApiClient to Client. Make available via qencode.Client Class instances should start with capital letters and always the pure object being classified if possible (for code completion, api interlinking, type annotations, etc) --- README.md | 2 +- docs/quickstart.md | 2 +- docs/usage.md | 4 ++-- qencode/__init__.py | 3 ++- qencode/client.py | 2 +- sample-code/start_custom_hls.py | 2 +- sample-code/start_custom_mp4.py | 2 +- sample-code/start_custom_with_dict.py | 2 +- sample-code/start_custom_with_file.py | 4 ++-- sample-code/start_custom_with_json.py | 2 +- sample-code/start_encode.py | 2 +- sample-code/start_with_callback.py | 2 +- 12 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9960360..422ef20 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ poetry install ```python import qencode -client = qencode.client(API_KEY) +client = qencode.Client(api_key=API_KEY) client.create() task = client.create_task() diff --git a/docs/quickstart.md b/docs/quickstart.md index dca1bb4..86e690c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -27,7 +27,7 @@ poetry install ```python import qencode -client = qencode.client(API_KEY) +client = qencode.Client(api_key=API_KEY) client.create() task = client.create_task() diff --git a/docs/usage.md b/docs/usage.md index 94b9eda..a493b34 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -15,7 +15,7 @@ VIDEO_URL = 'your source url' def start_encode(): - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) client.create() if client.error: print 'encoder error:', client.error, client.message @@ -102,7 +102,7 @@ def start_encode(): :param api_version: int. not required. default 'v1' :return: client object """ - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) client.create() if client.error: print('encoder error:', client.error, client.message) diff --git a/qencode/__init__.py b/qencode/__init__.py index a9f4070..2593857 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -1,3 +1,4 @@ +from .client import Client from .custom_params import ( CustomTranscodingParams, Destination, @@ -12,7 +13,7 @@ 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) + return Client(api_key, api_url=api_url, version=version, **kwargs) __version__ = "1.0" diff --git a/qencode/client.py b/qencode/client.py index 7e0d3f8..9a88083 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -3,7 +3,7 @@ from task import Task -class QencodeApiClient(object): +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/' diff --git a/sample-code/start_custom_hls.py b/sample-code/start_custom_hls.py index b481c67..af575f4 100644 --- a/sample-code/start_custom_hls.py +++ b/sample-code/start_custom_hls.py @@ -52,7 +52,7 @@ def start_encode(): :return: task object """ - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) if client.error: raise QencodeClientException(client.message) diff --git a/sample-code/start_custom_mp4.py b/sample-code/start_custom_mp4.py index fb97fd7..9c699c8 100644 --- a/sample-code/start_custom_mp4.py +++ b/sample-code/start_custom_mp4.py @@ -46,7 +46,7 @@ def start_encode(): :return: task object """ - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) if client.error: raise QencodeClientException(client.message) diff --git a/sample-code/start_custom_with_dict.py b/sample-code/start_custom_with_dict.py index b325597..89662ce 100644 --- a/sample-code/start_custom_with_dict.py +++ b/sample-code/start_custom_with_dict.py @@ -40,7 +40,7 @@ def start_encode(): :return: task object """ - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) if client.error: raise QencodeClientException(client.message) diff --git a/sample-code/start_custom_with_file.py b/sample-code/start_custom_with_file.py index 15606d7..8bb3877 100644 --- a/sample-code/start_custom_with_file.py +++ b/sample-code/start_custom_with_file.py @@ -42,9 +42,9 @@ def start_encode(): :param api_url: string. not required :param api_version: int. not required. default 'v1' :return: task object - """ + """ - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) if client.error: raise QencodeClientException(client.message) diff --git a/sample-code/start_custom_with_json.py b/sample-code/start_custom_with_json.py index 993fe92..c4c5368 100644 --- a/sample-code/start_custom_with_json.py +++ b/sample-code/start_custom_with_json.py @@ -40,7 +40,7 @@ def start_encode(): :return: task object """ - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) if client.error: raise QencodeClientException(client.message) diff --git a/sample-code/start_encode.py b/sample-code/start_encode.py index be0af14..fc1b2c8 100644 --- a/sample-code/start_encode.py +++ b/sample-code/start_encode.py @@ -36,7 +36,7 @@ def start_encode(): :return: task object """ - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) if client.error: raise QencodeClientException(client.message) diff --git a/sample-code/start_with_callback.py b/sample-code/start_with_callback.py index 7271e3f..9eada39 100644 --- a/sample-code/start_with_callback.py +++ b/sample-code/start_with_callback.py @@ -43,7 +43,7 @@ def start_encode(): :return: task object """ - client = qencode.client(API_KEY) + client = qencode.Client(api_key=API_KEY) if client.error: raise QencodeClientException(client.message) From 7aa0c3f20284dcbf06c3bdb9727a4881fa84eb85 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:25:08 -0500 Subject: [PATCH 35/47] Fix httptools (relative import, its not the pypi package of the same name) --- qencode/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qencode/client.py b/qencode/client.py index 9a88083..de4c03c 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -1,7 +1,8 @@ -from httptools import Http -from metadata import Metadata from task import Task +from .httptools import Http +from .metadata import Metadata + class Client(object): def __init__(self, api_key, api_url=None, version=None): From c933b6950cf635a770c861a4fde329c43572f34b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:26:08 -0500 Subject: [PATCH 36/47] Fix task, which is relative import --- qencode/client.py | 3 +-- qencode/metadata.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/qencode/client.py b/qencode/client.py index de4c03c..d7f5f9c 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -1,7 +1,6 @@ -from task import Task - from .httptools import Http from .metadata import Metadata +from .task import Task class Client(object): diff --git a/qencode/metadata.py b/qencode/metadata.py index c9d59b9..be66769 100644 --- a/qencode/metadata.py +++ b/qencode/metadata.py @@ -1,8 +1,7 @@ import urllib2 -from task import * - from .exceptions import QencodeTaskException +from .task import Task class Metadata(Task): From d77a4f2617f07c4891a629d8882ca74b1b7fcd4a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:39:35 -0500 Subject: [PATCH 37/47] httptools: Remove need for urllib2, make python 2/3 compatible --- qencode/_compat.py | 6 ++++++ qencode/httptools.py | 15 +++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/qencode/_compat.py b/qencode/_compat.py index 364432e..5921b3c 100644 --- a/qencode/_compat.py +++ b/qencode/_compat.py @@ -6,5 +6,11 @@ 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/httptools.py b/qencode/httptools.py index fe82e79..dfe701e 100644 --- a/qencode/httptools.py +++ b/qencode/httptools.py @@ -1,7 +1,6 @@ import json -import urllib -import urllib2 -from urlparse import urljoin + +from ._compat import HTTPError, Request, URLError, urlencode, urljoin, urlopen class Http(object): @@ -14,18 +13,18 @@ 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) + data = urlencode(post_data) + request = Request(url, data) try: - res = urllib2.urlopen(request) - except urllib2.HTTPError as e: + 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 urllib2.URLError as e: + except URLError as e: response = dict(error=True, message='URLError: {0}'.format(e.reason)) response = json.dumps(response) else: From 09b9a4cf597c1f2ceac39d676f80fc41406e31c8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:41:46 -0500 Subject: [PATCH 38/47] flake8: Ignore build/ directory --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4ab22d5..45e1226 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [flake8] -exclude = .*/,.tox,*.egg,qencode/_compat.py,qencode/__*__.py, +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 From 636aa2f58c5ece0f121425cef1665d75599bb497 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:48:55 -0500 Subject: [PATCH 39/47] Allow action to continue even if codecov fails --- .github/workflows/qencode-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/qencode-ci.yml b/.github/workflows/qencode-ci.yml index 6e41a69..b251b7b 100644 --- a/.github/workflows/qencode-ci.yml +++ b/.github/workflows/qencode-ci.yml @@ -30,5 +30,6 @@ jobs: run: | poetry run py.test --cov=./ --cov-report=xml - uses: codecov/codecov-action@v1 + continue-on-error: true with: token: ${{ secrets.CODECOV_TOKEN }} From 0cd21e103237e671005032228201a198a6e39abc Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 06:56:59 -0500 Subject: [PATCH 40/47] httptools: Fix encoding issue with request post data (2/3 compat) --- qencode/httptools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qencode/httptools.py b/qencode/httptools.py index dfe701e..5802a93 100644 --- a/qencode/httptools.py +++ b/qencode/httptools.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import json from ._compat import HTTPError, Request, URLError, urlencode, urljoin, urlopen @@ -13,7 +15,7 @@ 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) + data = urlencode(post_data).encode("utf-8") request = Request(url, data) try: res = urlopen(request) From ddcd1f97e227ab4f6bcc58fe6cbd9caaae850075 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Jul 2020 07:09:35 -0500 Subject: [PATCH 41/47] Fix python 3 mutation issue In python 3, mutating a dict in a for loop will raise a RuntimeError --- qencode/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qencode/utils.py b/qencode/utils.py index 81aafa2..8395ec1 100644 --- a/qencode/utils.py +++ b/qencode/utils.py @@ -28,9 +28,10 @@ def is_json(value): 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): From 3789030efcf716ac45dab14a8dacc4a5e33ac535 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 13 Jul 2020 06:13:05 -0500 Subject: [PATCH 42/47] metadata: import time --- qencode/metadata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qencode/metadata.py b/qencode/metadata.py index be66769..8c9cffc 100644 --- a/qencode/metadata.py +++ b/qencode/metadata.py @@ -1,3 +1,4 @@ +import time import urllib2 from .exceptions import QencodeTaskException From 02f743f2569d75d5854f6cc286e41f93bff3eff8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 13 Jul 2020 06:14:23 -0500 Subject: [PATCH 43/47] metadata: Remove need for urllib2, add 2/3 compatibility --- qencode/metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qencode/metadata.py b/qencode/metadata.py index 8c9cffc..2986448 100644 --- a/qencode/metadata.py +++ b/qencode/metadata.py @@ -1,6 +1,6 @@ import time -import urllib2 +from ._compat import urlopen from .exceptions import QencodeTaskException from .task import Task @@ -38,6 +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 From d892a4ddd5fca896d0777b8aafe90b3a6e5376e3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 14 Jul 2020 09:11:56 -0500 Subject: [PATCH 44/47] Query: Python 2+3 compatibility --- qencode/custom_params.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qencode/custom_params.py b/qencode/custom_params.py index 3c5d2b9..ac5eec6 100644 --- a/qencode/custom_params.py +++ b/qencode/custom_params.py @@ -1,6 +1,7 @@ import json from json import JSONEncoder +from ._compat import PY2 from .utils import rm_attributes_if_null @@ -128,7 +129,8 @@ def __init__(self): def prepare_params(self): query = dict(query=self.params) try: - self.query = json.dumps(query, cls=MyEncoder, encoding='utf-8') + 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) From 0e5db8e4cb7ac85258fe981f3ef95eb49b99f269 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 28 Jul 2020 10:17:16 -0500 Subject: [PATCH 45/47] setup.py: Add Python 3.6-3.8 to trove classifiers --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 063d62d..3afdef0 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,9 @@ 'Topic :: Software Development :: Build Tools', 'License :: Other/Proprietary License', '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'], From 1107cb156e6c938630e0b8b5a903aeb380d61153 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 1 Oct 2020 10:05:47 -0500 Subject: [PATCH 46/47] Add setuptools as build dependency See also: https://github.com/pypa/pip/issues/6434 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ff5a125..7ec1be8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,5 +38,5 @@ pytest-mock = [ ] [build-system] -requires = ["poetry>=0.12"] +requires = ["poetry>=0.12", "setuptools"] build-backend = "poetry.masonry.api" From 531d4e58f1b91603d574cc5fc3c307b494a53335 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 1 Oct 2020 10:10:30 -0500 Subject: [PATCH 47/47] Pin poetry < 1.1 until 1.1 stabilizes --- .github/workflows/qencode-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qencode-ci.yml b/.github/workflows/qencode-ci.yml index b251b7b..9b41f45 100644 --- a/.github/workflows/qencode-ci.yml +++ b/.github/workflows/qencode-ci.yml @@ -21,7 +21,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade poetry + python -m pip install --upgrade 'poetry<1.1' poetry install - name: Lint with flake8 run: |