diff --git a/.darglint b/.darglint new file mode 100644 index 00000000..2b03755a --- /dev/null +++ b/.darglint @@ -0,0 +1,2 @@ +[darglint] +strictness = short diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..85628c2e --- /dev/null +++ b/.flake8 @@ -0,0 +1,15 @@ +[flake8] +select = B,B9,C,D,DAR,E,F,N,RST,S,W +# Some rules are ignore on top of the standard ones. +# C901 (complexity) will be processed in a dedicated PR +# DARxxx (documentation in docstrings) will be processed in a dedicated PR +# Final target is: +# ignore = E203,E501,RST201,RST203,RST301,W503 +ignore = E203,E501,RST201,RST203,RST301,W503, C901, DAR101, DAR201 +max-line-length = 80 +max-complexity = 10 +docstring-convention = google +per-file-ignores = + tests/*:S101 + tests/**/const_*.py:B950 + src/synology_dsm/const.py:B950 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..f1b045a8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,13 @@ +@Quentame + +.github/* @oncleben31 +.darglint @oncleben31 +.flake8 @oncleben31 + +*/surveillance_station/* @shenxn +src/synology_dsm/api/core/share.py @Gestas +src/synology_dsm/api/core/system.py @mib1185 +src/synology_dsm/api/core/upgrade.py @mib1185 +tests/api_data/dsm_6/core/const_6_core_share.py @Gestas +tests/api_data/dsm_6/core/const_6_core_system.py @mib1185 +tests/api_data/dsm_6/core/const_6_core_upgrade.py @mib1185 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..a0a5c735 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + - package-ecosystem: pip + directory: "/.github/workflows" + schedule: + interval: daily + - package-ecosystem: pip + directory: "/docs" + schedule: + interval: daily + - package-ecosystem: pip + directory: "/" + schedule: + interval: daily diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..7a04410f --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,29 @@ +categories: + - title: ":boom: Breaking Changes" + label: "breaking" + - title: ":rocket: Features" + label: "enhancement" + - title: ":fire: Removals and Deprecations" + label: "removal" + - title: ":beetle: Fixes" + label: "bug" + - title: ":racehorse: Performance" + label: "performance" + - title: ":rotating_light: Testing" + label: "testing" + - title: ":construction_worker: Continuous Integration" + label: "ci" + - title: ":books: Documentation" + label: "documentation" + - title: ":hammer: Refactoring" + label: "refactoring" + - title: ":lipstick: Style" + label: "style" + - title: ":package: Dependencies" + labels: + - "dependencies" + - "build" +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt new file mode 100644 index 00000000..2b085eac --- /dev/null +++ b/.github/workflows/constraints.txt @@ -0,0 +1,5 @@ +pip==20.2.4 +nox==2020.8.22 +nox-poetry==0.5.0 +poetry==1.1.4 +virtualenv==20.1.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..2970ae46 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,79 @@ +name: Release + +on: + push: + branches: + - main + - master + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v2.3.4 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@v2.1.4 + with: + python-version: "3.9" + + - name: Upgrade pip + run: | + pip install --constraint=.github/workflows/constraints.txt pip + pip --version + + - name: Install Poetry + run: | + pip install --constraint=.github/workflows/constraints.txt poetry + poetry --version + + - name: Check if there is a parent commit + id: check-parent-commit + run: | + echo "::set-output name=sha::$(git rev-parse --verify --quiet HEAD^)" + + - name: Detect and tag new version + id: check-version + if: steps.check-parent-commit.outputs.sha + uses: salsify/action-detect-and-tag-new-version@v2.0.1 + with: + version-command: | + bash -o pipefail -c "poetry version | awk '{ print \$2 }'" + + - name: Bump version for developmental release + if: "! steps.check-version.outputs.tag" + run: | + poetry version patch && + version=$(poetry version | awk '{ print $2 }') && + poetry version $version.dev.$(date +%s) + + - name: Build package + run: | + poetry build --ansi + + - name: Publish package on PyPI + if: steps.check-version.outputs.tag + uses: pypa/gh-action-pypi-publish@v1.4.1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + + - name: Publish package on TestPyPI + if: "! steps.check-version.outputs.tag" + uses: pypa/gh-action-pypi-publish@v1.4.1 + with: + user: __token__ + password: ${{ secrets.TEST_PYPI_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + + - name: Publish release notes + uses: release-drafter/release-drafter@v5.12.1 + with: + publish: ${{ steps.check-version.outputs.tag != '' }} + tag: ${{ steps.check-version.outputs.tag }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..bb544907 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,140 @@ +name: Tests + +on: + - push + - pull_request + +jobs: + tests: + name: ${{ matrix.session }} ${{ matrix.python-version }} / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # Commented session will be activated in the future + - { python-version: 3.9, os: ubuntu-latest, session: "pre-commit" } + - { python-version: 3.9, os: ubuntu-latest, session: "safety" } + # - { python-version: 3.9, os: ubuntu-latest, session: "mypy" } + # - { python-version: 3.8, os: ubuntu-latest, session: "mypy" } + # - { python-version: 3.7, os: ubuntu-latest, session: "mypy" } + - { python-version: 3.9, os: ubuntu-latest, session: "tests" } + - { python-version: 3.8, os: ubuntu-latest, session: "tests" } + - { python-version: 3.7, os: ubuntu-latest, session: "tests" } + - { python-version: 3.9, os: windows-latest, session: "tests" } + - { python-version: 3.9, os: macos-latest, session: "tests" } + # - { python-version: 3.9, os: ubuntu-latest, session: "typeguard" } + # - { python-version: 3.9, os: ubuntu-latest, session: "xdoctest" } + # - { python-version: 3.8, os: ubuntu-latest, session: "docs-build" } + + env: + NOXSESSION: ${{ matrix.session }} + + steps: + - name: Check out the repository + uses: actions/checkout@v2.3.4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2.1.4 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade pip + run: | + pip install --constraint=.github/workflows/constraints.txt pip + pip --version + + - name: Install Poetry + run: | + pip install --constraint=.github/workflows/constraints.txt poetry + poetry --version + + - name: Install Nox + run: | + pip install --constraint=.github/workflows/constraints.txt nox nox-poetry + nox --version + + - name: Compute pre-commit cache key + if: matrix.session == 'pre-commit' + id: pre-commit-cache + shell: python + run: | + import hashlib + import sys + + python = "py{}.{}".format(*sys.version_info[:2]) + payload = sys.version.encode() + sys.executable.encode() + digest = hashlib.sha256(payload).hexdigest() + result = "${{ runner.os }}-{}-{}-pre-commit".format(python, digest[:8]) + + print("::set-output name=result::{}".format(result)) + + - name: Restore pre-commit cache + uses: actions/cache@v2.1.3 + if: matrix.session == 'pre-commit' + with: + path: ~/.cache/pre-commit + key: ${{ steps.pre-commit-cache.outputs.result }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + ${{ steps.pre-commit-cache.outputs.result }}- + + - name: Run Nox + run: | + nox --force-color --python=${{ matrix.python-version }} + + - name: Upload coverage data + if: always() && matrix.session == 'tests' + uses: "actions/upload-artifact@v2.2.1" + with: + name: coverage-data + path: ".coverage.*" + + - name: Upload documentation + if: matrix.session == 'docs-build' + uses: actions/upload-artifact@v2.2.1 + with: + name: docs + path: docs/_build + + coverage: + runs-on: ubuntu-latest + needs: tests + steps: + - name: Check out the repository + uses: actions/checkout@v2.3.4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v2.1.4 + with: + python-version: 3.9 + + - name: Upgrade pip + run: | + pip install --constraint=.github/workflows/constraints.txt pip + pip --version + + - name: Install Poetry + run: | + pip install --constraint=.github/workflows/constraints.txt poetry + poetry --version + + - name: Install Nox + run: | + pip install --constraint=.github/workflows/constraints.txt nox nox-poetry + nox --version + + - name: Download coverage data + uses: actions/download-artifact@v2.0.6 + with: + name: coverage-data + + - name: Combine coverage data and display human readable report + run: | + nox --force-color --session=coverage + + - name: Create coverage report + run: | + nox --force-color --session=coverage -- xml + + - name: Upload coverage report + uses: codecov/codecov-action@v1.0.15 diff --git a/.gitignore b/.gitignore index f2ec26cd..d62b0712 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ +# From cookiecutter-hypermodern-python +.mypy_cache/ +/.coverage +/.nox/ +/.python-version +/.pytype/ +/dist/ +/docs/_build/ +/src/*.egg-info/ +__pycache__/ + +# Following are kept for not anoying current developers. Could be remove in +# a future package release. + # Python *.py[cod] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b6652b19 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,51 @@ +repos: + - repo: local + hooks: + - id: black + name: black + entry: black + language: system + types: [python] + require_serial: true + - id: check-added-large-files + name: Check for added large files + entry: check-added-large-files + language: system + - id: check-toml + name: Check Toml + entry: check-toml + language: system + types: [toml] + - id: check-yaml + name: Check Yaml + entry: check-yaml + language: system + types: [yaml] + - id: end-of-file-fixer + name: Fix End of Files + entry: end-of-file-fixer + language: system + types: [text] + stages: [commit, push, manual] + - id: flake8 + name: flake8 + entry: flake8 + language: system + types: [python] + require_serial: true + - id: reorder-python-imports + name: Reorder python imports + entry: reorder-python-imports + language: system + types: [python] + args: [--application-directories=src] + - id: trailing-whitespace + name: Trim Trailing Whitespace + entry: trailing-whitespace-fixer + language: system + types: [text] + stages: [commit, push, manual] + - repo: https://github.com/prettier/prettier + rev: 2.1.2 + hooks: + - id: prettier diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c1a73ea7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -os: linux -dist: xenial - -language: python -python: - - 3.6 - - 3.7 - - 3.8 -cache: - pip: true - -before_install: - - pip install -r requirements_all.txt - - pip install -e . -install: - - python setup.py install - - python setup.py sdist -before_script: - - pylint synology_dsm tests - - black --check --fast . -script: - - py.test - -deploy: - - provider: pypi - user: $pypi_test_user - password: $pypi_test_pass - server: $pypi_test - on: - branch: deploy-test - python: 3.8 - - provider: pypi - user: $pypi_user - password: $pypi_pass - server: $pypi - on: - branch: deploy - python: 3.8 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..640022ad --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,121 @@ +Contributor Guide +================= + +Thank you for your interest in improving this project. +This project is open-source under the `MIT license`_ and +welcomes contributions in the form of bug reports, feature requests, and pull requests. + +Here is a list of important resources for contributors: + +- `Source Code`_ +- `Documentation`_ +- `Issue Tracker`_ +- `Code of Conduct`_ + +.. _MIT license: https://opensource.org/licenses/MIT +.. _Source Code: https://github.com/hacf-fr/synologydsm-api +.. _Documentation: https://synologydsm-api.readthedocs.io/ +.. _Issue Tracker: https://github.com/hacf-fr/synologydsm-api/issues + +How to report a bug +------------------- + +Report bugs on the `Issue Tracker`_. + +When filing an issue, make sure to answer these questions: + +- Which operating system and Python version are you using? +- Which version of this project are you using? +- What did you do? +- What did you expect to see? +- What did you see instead? + +The best way to get your bug fixed is to provide a test case, +and/or steps to reproduce the issue. + + +How to request a feature +------------------------ + +Request features on the `Issue Tracker`_. + + +How to set up your development environment +------------------------------------------ + +You need Python 3.6+ and the following tools: + +- Poetry_ +- Nox_ +- nox-poetry_ + +Install the package with development requirements: + +.. code:: console + + $ poetry install + +You can now run an interactive Python session: + +.. code:: console + + $ poetry run python + +.. _Poetry: https://python-poetry.org/ +.. _Nox: https://nox.thea.codes/ +.. _nox-poetry: https://nox-poetry.readthedocs.io/ + + +How to test the project +----------------------- + +Run the full test suite: + +.. code:: console + + $ nox + +List the available Nox sessions: + +.. code:: console + + $ nox --list-sessions + +You can also run a specific Nox session. +For example, invoke the unit test suite like this: + +.. code:: console + + $ nox --session=tests + +Unit tests are located in the ``tests`` directory, +and are written using the pytest_ testing framework. + +.. _pytest: https://pytest.readthedocs.io/ + + +How to submit changes +--------------------- + +Open a `pull request`_ to submit changes to this project. + +Your pull request needs to meet the following guidelines for acceptance: + +- The Nox test suite must pass without errors and warnings. +- Include unit tests. This project maintains 100% code coverage. +- If your changes add functionality, update the documentation accordingly. + +Feel free to submit early, though—we can always iterate on this. + +To run linting and code formatting checks before commiting your change, you can install pre-commit as a Git hook by running the following command: + +.. code:: console + + $ nox --session=pre-commit -- install + +It is recommended to open an issue before starting work on anything. +This will allow a chance to talk it over with the owners and validate your approach. + +.. _pull request: https://github.com/hacf-fr/synologydsm-api/pulls +.. github-only +.. _Code of Conduct: CODE_OF_CONDUCT.rst diff --git a/LICENSE.txt b/LICENSE.txt index cc44b79e..76a4fde2 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,8 +1,8 @@ The MIT License (MIT) -Copyright (c) 2016 ProtoThis +Copyright (c) 2020 hacf-fr Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.rst b/README.rst index ddba2dc3..b03430d3 100644 --- a/README.rst +++ b/README.rst @@ -2,20 +2,20 @@ Python API for Synology DSM =========================== -.. image:: https://travis-ci.org/ProtoThis/python-synology.svg?branch=master - :target: https://travis-ci.org/ProtoThis/python-synology +.. image:: https://travis-ci.org/hacf-fr/synologydsm-api.svg?branch=master + :target: https://travis-ci.org/hacf-fr/synologydsm-api -.. image:: https://img.shields.io/pypi/v/python-synology.svg +.. image:: https://img.shields.io/pypi/v/synologydsm-api.svg :alt: Library version - :target: https://pypi.org/project/python-synology + :target: https://pypi.org/project/synologydsm-api -.. image:: https://img.shields.io/pypi/pyversions/python-synology.svg +.. image:: https://img.shields.io/pypi/pyversions/synologydsm-api.svg :alt: Supported versions - :target: https://pypi.org/project/python-synology + :target: https://pypi.org/project/synologydsm-api -.. image:: https://pepy.tech/badge/python-synology +.. image:: https://pepy.tech/badge/synologydsm-api :alt: Downloads - :target: https://pypi.org/project/python-synology + :target: https://pypi.org/project/synologydsm-api .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :alt: Formated with Black @@ -27,7 +27,7 @@ Installation .. code-block:: bash - [sudo] pip install python-synology + [sudo] pip install synologydsm-api Usage @@ -47,6 +47,7 @@ Constructor username, password, use_https=False, + verify_ssl=False, timeout=None, device_token=None, debugmode=False, @@ -251,6 +252,16 @@ Upgrade usage if upgrade.update_available: do something ... + # get available version string (return None if no update available) + upgrade.available_version + + # get need of reboot (return None if no update available) + upgrade.reboot_needed + + # get need of service restarts (return None if no update available) + upgrade.service_restarts + + Credits / Special Thanks ======================== - https://github.com/florianeinfalt diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..d3f978c9 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +comment: false +coverage: + status: + project: + default: + target: "80" + patch: + default: + target: "100" diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..4f2123c9 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,20 @@ +[mypy] +check_untyped_defs = True +disallow_any_generics = True +disallow_incomplete_defs = True +disallow_subclassing_any = True +disallow_untyped_calls = True +disallow_untyped_decorators = True +disallow_untyped_defs = True +no_implicit_optional = True +no_implicit_reexport = True +pretty = True +show_column_numbers = True +show_error_codes = True +show_error_context = True +strict_equality = True +warn_redundant_casts = True +warn_return_any = True +warn_unreachable = True +warn_unused_configs = True +warn_unused_ignores = True diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..7b6894e9 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,188 @@ +"""Nox sessions.""" +import shutil +import sys +from pathlib import Path +from textwrap import dedent + +import nox +import nox_poetry.patch +from nox.sessions import Session + + +package = "synology_dsm" +python_versions = ["3.9", "3.8", "3.7"] +# Comment some sessions as they need to work on the code to not fail. +# Will be activated in future release. +nox.options.sessions = ( + "pre-commit", + "safety", + # "mypy", + "tests", + # "typeguard", + # "xdoctest", + # "docs-build", +) + + +def activate_virtualenv_in_precommit_hooks(session: Session) -> None: + """Activate virtualenv in hooks installed by pre-commit. + + This function patches git hooks installed by pre-commit to activate the + session's virtual environment. This allows pre-commit to locate hooks in + that environment when invoked from git. + + Args: + session: The Session object. + """ + if session.bin is None: + return + + virtualenv = session.env.get("VIRTUAL_ENV") + if virtualenv is None: + return + + hookdir = Path(".git") / "hooks" + if not hookdir.is_dir(): + return + + for hook in hookdir.iterdir(): + if hook.name.endswith(".sample") or not hook.is_file(): + continue + + text = hook.read_text() + bindir = repr(session.bin)[1:-1] # strip quotes + if not ( + Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text + ): + continue + + lines = text.splitlines() + if not (lines[0].startswith("#!") and "python" in lines[0].lower()): + continue + + header = dedent( + f"""\ + import os + os.environ["VIRTUAL_ENV"] = {virtualenv!r} + os.environ["PATH"] = os.pathsep.join(( + {session.bin!r}, + os.environ.get("PATH", ""), + )) + """ + ) + + lines.insert(1, header) + hook.write_text("\n".join(lines)) + + +@nox.session(name="pre-commit", python="3.9") +def precommit(session: Session) -> None: + """Lint using pre-commit.""" + args = session.posargs or ["run", "--all-files", "--show-diff-on-failure"] + session.install( + "black", + "darglint", + "flake8", + "flake8-bandit", + "flake8-bugbear", + "flake8-docstrings", + "flake8-rst-docstrings", + "pep8-naming", + "pre-commit", + "pre-commit-hooks", + "reorder-python-imports", + ) + session.run("pre-commit", *args) + if args and args[0] == "install": + activate_virtualenv_in_precommit_hooks(session) + + +@nox.session(python="3.9") +def safety(session: Session) -> None: + """Scan dependencies for insecure packages.""" + requirements = nox_poetry.export_requirements(session) + session.install("safety") + session.run("safety", "check", f"--file={requirements}", "--bare") + + +@nox.session(python=python_versions) +def mypy(session: Session) -> None: + """Type-check using mypy.""" + args = session.posargs or ["src", "tests"] + session.install(".") + session.install("mypy", "pytest") + session.run("mypy", *args) + if not session.posargs: + session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py") + + +@nox.session(python=python_versions) +def tests(session: Session) -> None: + """Run the test suite.""" + session.install(".") + session.install("coverage[toml]", "pytest", "pygments") + try: + session.run("coverage", "run", "--parallel", "-m", "pytest", *session.posargs) + finally: + if session.interactive: + session.notify("coverage") + + +@nox.session +def coverage(session: Session) -> None: + """Produce the coverage report.""" + # Do not use session.posargs unless this is the only session. + has_args = session.posargs and len(session._runner.manifest) == 1 + args = session.posargs if has_args else ["report"] + + session.install("coverage[toml]") + + if not has_args and any(Path().glob(".coverage.*")): + session.run("coverage", "combine") + + session.run("coverage", *args) + + +@nox.session(python=python_versions) +def typeguard(session: Session) -> None: + """Runtime type checking using Typeguard.""" + session.install(".") + session.install("pytest", "typeguard", "pygments") + session.run("pytest", f"--typeguard-packages={package}", *session.posargs) + + +@nox.session(python=python_versions) +def xdoctest(session: Session) -> None: + """Run examples with xdoctest.""" + args = session.posargs or ["all"] + session.install(".") + session.install("xdoctest[colors]") + session.run("python", "-m", "xdoctest", package, *args) + + +@nox.session(name="docs-build", python="3.8") +def docs_build(session: Session) -> None: + """Build the documentation.""" + args = session.posargs or ["docs", "docs/_build"] + session.install(".") + session.install("sphinx", "sphinx-rtd-theme") + + build_dir = Path("docs", "_build") + if build_dir.exists(): + shutil.rmtree(build_dir) + + session.run("sphinx-build", *args) + + +@nox.session(python="3.8") +def docs(session: Session) -> None: + """Build and serve the documentation with live reloading on file changes.""" + args = session.posargs or ["--open-browser", "docs", "docs/_build"] + session.install(".") + session.install("sphinx", "sphinx-autobuild", "sphinx-rtd-theme") + + build_dir = Path("docs", "_build") + if build_dir.exists(): + shutil.rmtree(build_dir) + + session.run("sphinx-autobuild", *args) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..e4aa9913 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1477 @@ +[[package]] +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "aspy.refactor-imports" +version = "2.1.1" +description = "Utilities for refactoring imports in python-like syntax." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +cached-property = "*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "20.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "babel" +version = "2.8.0" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +name = "bandit" +version = "1.6.2" +description = "Security oriented static analyser for python code." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=3.13" +six = ">=1.10.0" +stevedore = ">=1.20.0" + +[[package]] +name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "certifi" +version = "2020.6.20" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cfgv" +version = "3.2.0" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "5.3" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.dependencies] +toml = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["toml"] + +[[package]] +name = "darglint" +version = "1.5.5" +description = "A utility for ensuring Google-style docstrings stay up to date with the source code." +category = "dev" +optional = false +python-versions = ">=3.5,<4.0" + +[[package]] +name = "distlib" +version = "0.3.1" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "docutils" +version = "0.16" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "dparse" +version = "0.5.1" +description = "A parser for Python dependency files" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +packaging = "*" +pyyaml = "*" +toml = "*" + +[package.extras] +pipenv = ["pipenv"] + +[[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "flake8" +version = "3.8.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[[package]] +name = "flake8-bandit" +version = "2.1.2" +description = "Automated security testing with bandit and flake8." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +bandit = "*" +flake8 = "*" +flake8-polyfill = "*" +pycodestyle = "*" + +[[package]] +name = "flake8-bugbear" +version = "20.1.4" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[[package]] +name = "flake8-docstrings" +version = "1.5.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "flake8-polyfill" +version = "1.0.2" +description = "Polyfill package for Flake8 plugins" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-rst-docstrings" +version = "0.0.14" +description = "Python docstring reStructuredText (RST) validator" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3.0.0" +restructuredtext_lint = "*" + +[[package]] +name = "gitdb" +version = "4.0.5" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +smmap = ">=3.0.1,<4" + +[[package]] +name = "gitpython" +version = "3.1.9" +description = "Python Git Library" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "identify" +version = "1.5.6" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.extras] +license = ["editdistance"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "imagesize" +version = "1.2.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "importlib-metadata" +version = "2.0.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "jinja2" +version = "2.11.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +name = "livereload" +version = "2.6.3" +description = "Python LiveReload is an awesome tool for web developers" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" +tornado = {version = "*", markers = "python_version > \"2.7\""} + +[[package]] +name = "markupsafe" +version = "1.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy" +version = "0.790" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nodeenv" +version = "1.5.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "20.4" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +name = "pathspec" +version = "0.8.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pbr" +version = "5.5.1" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "pep8-naming" +version = "0.11.1" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8-polyfill = ">=1.0.2,<2" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "pre-commit" +version = "2.8.2" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + +[[package]] +name = "pre-commit-hooks" +version = "3.3.0" +description = "Some out-of-the-box hooks for pre-commit." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +"ruamel.yaml" = ">=0.15" +toml = "*" + +[[package]] +name = "py" +version = "1.9.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydocstyle" +version = "5.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +snowballstemmer = "*" + +[[package]] +name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.7.2" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +checkqa_mypy = ["mypy (==0.780)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytz" +version = "2020.1" +description = "World timezone definitions, modern and historical" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "regex" +version = "2020.10.15" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "reorder-python-imports" +version = "2.3.6" +description = "Tool for reordering python imports" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +"aspy.refactor-imports" = ">=2.1.0" + +[[package]] +name = "requests" +version = "2.25.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "restructuredtext-lint" +version = "1.3.1" +description = "reStructuredText linter" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +docutils = ">=0.11,<1.0" + +[[package]] +name = "ruamel.yaml" +version = "0.16.12" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.9\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel.yaml.clib" +version = "0.2.2" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "safety" +version = "1.9.0" +description = "Checks installed dependencies for known vulnerabilities." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +Click = ">=6.0" +dparse = ">=0.5.1" +packaging = "*" +requests = "*" + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "3.0.4" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "snowballstemmer" +version = "2.0.0" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "sphinx" +version = "3.3.1" +description = "Python documentation generator" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.12" +imagesize = "*" +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +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.790)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] + +[[package]] +name = "sphinx-autobuild" +version = "2020.9.1" +description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +livereload = "*" +sphinx = "*" + +[package.extras] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "sphinx-rtd-theme" +version = "0.5.0" +description = "Read the Docs theme for Sphinx" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +sphinx = "*" + +[package.extras] +dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "1.0.3" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.4" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "stevedore" +version = "3.2.2" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "toml" +version = "0.10.1" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "tornado" +version = "6.0.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "dev" +optional = false +python-versions = ">= 3.5" + +[[package]] +name = "typed-ast" +version = "1.4.1" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typeguard" +version = "2.10.0" +description = "Run-time type checker for Python" +category = "dev" +optional = false +python-versions = ">=3.5.3" + +[package.extras] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["pytest", "typing-extensions"] + +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "urllib3" +version = "1.26.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.0.35" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +importlib-metadata = {version = ">=0.12,<3", markers = "python_version < \"3.8\""} +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[[package]] +name = "xdoctest" +version = "0.15.0" +description = "A rewrite of the builtin doctest module" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +colorama = {version = "*", optional = true, markers = "platform_system == \"Windows\" and extra == \"colors\""} +Pygments = {version = "*", optional = true, markers = "extra == \"colors\""} +six = "*" + +[package.extras] +all = ["six", "pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11", "pygments", "colorama", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel"] +colors = ["pygments", "colorama"] +jupyter = ["nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel"] +optional = ["pygments", "colorama", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel"] +tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel"] + +[[package]] +name = "zipp" +version = "3.3.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7.0" +content-hash = "c52e1e3a8b436b91d7fac920d8745e576575a079def6a53c6ceb8c98530197ad" + +[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"}, +] +"aspy.refactor-imports" = [ + {file = "aspy.refactor_imports-2.1.1-py2.py3-none-any.whl", hash = "sha256:9df76bf19ef81620068b785a386740ab3c8939fcbdcebf20c4a4e0057230d782"}, + {file = "aspy.refactor_imports-2.1.1.tar.gz", hash = "sha256:eec8d1a73bedf64ffb8b589ad919a030c1fb14acf7d1ce0ab192f6eedae895c5"}, +] +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-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, + {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, +] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, +] +bandit = [ + {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, + {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, +] +black = [ + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +cached-property = [ + {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, + {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, +] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +cfgv = [ + {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, + {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, +] +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"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, + {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"}, + {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"}, + {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"}, + {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"}, + {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"}, + {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"}, + {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"}, + {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"}, + {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"}, + {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"}, + {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"}, + {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"}, + {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"}, + {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"}, + {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"}, + {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"}, + {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"}, + {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"}, + {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, +] +darglint = [ + {file = "darglint-1.5.5-py3-none-any.whl", hash = "sha256:cd882c812f28ee3b5577259bfd8d6d25962386dd87fc1f3756eac24370aaa060"}, + {file = "darglint-1.5.5.tar.gz", hash = "sha256:2f12ce2ef3d8189279a8f2eb4c53fd215dbacae50e37765542a91310400a9cd6"}, +] +distlib = [ + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +dparse = [ + {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, + {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +flake8 = [ + {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, + {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, +] +flake8-bandit = [ + {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-20.1.4.tar.gz", hash = "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162"}, + {file = "flake8_bugbear-20.1.4-py36.py37.py38-none-any.whl", hash = "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63"}, +] +flake8-docstrings = [ + {file = "flake8-docstrings-1.5.0.tar.gz", hash = "sha256:3d5a31c7ec6b7367ea6506a87ec293b94a0a46c0bce2bb4975b7f1d09b6f3717"}, + {file = "flake8_docstrings-1.5.0-py2.py3-none-any.whl", hash = "sha256:a256ba91bc52307bef1de59e2a009c3cf61c3d0952dbe035d6ff7208940c2edc"}, +] +flake8-polyfill = [ + {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, + {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, +] +flake8-rst-docstrings = [ + {file = "flake8-rst-docstrings-0.0.14.tar.gz", hash = "sha256:8f8bcb18f1408b506dd8ba2c99af3eac6128f6911d4bf6ff874b94caa70182a2"}, +] +gitdb = [ + {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"}, + {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"}, +] +gitpython = [ + {file = "GitPython-3.1.9-py3-none-any.whl", hash = "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8"}, + {file = "GitPython-3.1.9.tar.gz", hash = "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"}, +] +identify = [ + {file = "identify-1.5.6-py2.py3-none-any.whl", hash = "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e"}, + {file = "identify-1.5.6.tar.gz", hash = "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421"}, +] +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-2.0.0-py2.py3-none-any.whl", hash = "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"}, + {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +livereload = [ + {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, +] +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"}, +] +mypy = [ + {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"}, + {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"}, + {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"}, + {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"}, + {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"}, + {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"}, + {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"}, + {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"}, + {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"}, + {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"}, + {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"}, + {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"}, + {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"}, + {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nodeenv = [ + {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, + {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, +] +pbr = [ + {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"}, + {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"}, +] +pep8-naming = [ + {file = "pep8-naming-0.11.1.tar.gz", hash = "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724"}, + {file = "pep8_naming-0.11.1-py2.py3-none-any.whl", hash = "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +pre-commit = [ + {file = "pre_commit-2.8.2-py2.py3-none-any.whl", hash = "sha256:22e6aa3bd571debb01eb7d34483f11c01b65237be4eebbf30c3d4fb65762d315"}, + {file = "pre_commit-2.8.2.tar.gz", hash = "sha256:905ebc9b534b991baec87e934431f2d0606ba27f2b90f7f652985f5a5b8b6ae6"}, +] +pre-commit-hooks = [ + {file = "pre_commit_hooks-3.3.0-py2.py3-none-any.whl", hash = "sha256:2190d72ac867bd9b8880de32d9304ec54182c89720cce56f22742890ed8ba90f"}, + {file = "pre_commit_hooks-3.3.0.tar.gz", hash = "sha256:1e18c0451279fb88653c7b9f8fd73ccc35925e95b636c5b64095538f68a23b06"}, +] +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"}, +] +pydocstyle = [ + {file = "pydocstyle-5.1.1-py3-none-any.whl", hash = "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"}, + {file = "pydocstyle-5.1.1.tar.gz", hash = "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325"}, +] +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.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"}, + {file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"}, +] +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-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"}, + {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, + {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +regex = [ + {file = "regex-2020.10.15-cp27-cp27m-win32.whl", hash = "sha256:e935a166a5f4c02afe3f7e4ce92ce5a786f75c6caa0c4ce09c922541d74b77e8"}, + {file = "regex-2020.10.15-cp27-cp27m-win_amd64.whl", hash = "sha256:d81be22d5d462b96a2aa5c512f741255ba182995efb0114e5a946fe254148df1"}, + {file = "regex-2020.10.15-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6d4cdb6c20e752426b2e569128488c5046fb1b16b1beadaceea9815c36da0847"}, + {file = "regex-2020.10.15-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:25991861c6fef1e5fd0a01283cf5658c5e7f7aa644128e85243bc75304e91530"}, + {file = "regex-2020.10.15-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:6e9f72e0ee49f7d7be395bfa29e9533f0507a882e1e6bf302c0a204c65b742bf"}, + {file = "regex-2020.10.15-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:578ac6379e65eb8e6a85299b306c966c852712c834dc7eef0ba78d07a828f67b"}, + {file = "regex-2020.10.15-cp36-cp36m-win32.whl", hash = "sha256:65b6b018b07e9b3b6a05c2c3bb7710ed66132b4df41926c243887c4f1ff303d5"}, + {file = "regex-2020.10.15-cp36-cp36m-win_amd64.whl", hash = "sha256:2f60ba5c33f00ce9be29a140e6f812e39880df8ba9cb92ad333f0016dbc30306"}, + {file = "regex-2020.10.15-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5d4a3221f37520bb337b64a0632716e61b26c8ae6aaffceeeb7ad69c009c404b"}, + {file = "regex-2020.10.15-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:26b85672275d8c7a9d4ff93dbc4954f5146efdb2ecec89ad1de49439984dea14"}, + {file = "regex-2020.10.15-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:828618f3c3439c5e6ef8621e7c885ca561bbaaba90ddbb6a7dfd9e1ec8341103"}, + {file = "regex-2020.10.15-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:aef23aed9d4017cc74d37f703d57ce254efb4c8a6a01905f40f539220348abf9"}, + {file = "regex-2020.10.15-cp37-cp37m-win32.whl", hash = "sha256:6c72adb85adecd4522a488a751e465842cdd2a5606b65464b9168bf029a54272"}, + {file = "regex-2020.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:ef3a55b16c6450574734db92e0a3aca283290889934a23f7498eaf417e3af9f0"}, + {file = "regex-2020.10.15-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8958befc139ac4e3f16d44ec386c490ea2121ed8322f4956f83dd9cad8e9b922"}, + {file = "regex-2020.10.15-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3dd952f3f8dc01b72c0cf05b3631e05c50ac65ddd2afdf26551638e97502107b"}, + {file = "regex-2020.10.15-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:608d6c05452c0e6cc49d4d7407b4767963f19c4d2230fa70b7201732eedc84f2"}, + {file = "regex-2020.10.15-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:02686a2f0b1a4be0facdd0d3ad4dc6c23acaa0f38fb5470d892ae88584ba705c"}, + {file = "regex-2020.10.15-cp38-cp38-win32.whl", hash = "sha256:137da580d1e6302484be3ef41d72cf5c3ad22a076070051b7449c0e13ab2c482"}, + {file = "regex-2020.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:20cdd7e1736f4f61a5161aa30d05ac108ab8efc3133df5eb70fe1e6a23ea1ca6"}, + {file = "regex-2020.10.15-cp39-cp39-manylinux1_i686.whl", hash = "sha256:85b733a1ef2b2e7001aff0e204a842f50ad699c061856a214e48cfb16ace7d0c"}, + {file = "regex-2020.10.15-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:af1f5e997dd1ee71fb6eb4a0fb6921bf7a778f4b62f1f7ef0d7445ecce9155d6"}, + {file = "regex-2020.10.15-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:b5eeaf4b5ef38fab225429478caf71f44d4a0b44d39a1aa4d4422cda23a9821b"}, + {file = "regex-2020.10.15-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:aeac7c9397480450016bc4a840eefbfa8ca68afc1e90648aa6efbfe699e5d3bb"}, + {file = "regex-2020.10.15-cp39-cp39-win32.whl", hash = "sha256:698f8a5a2815e1663d9895830a063098ae2f8f2655ae4fdc5dfa2b1f52b90087"}, + {file = "regex-2020.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:a51e51eecdac39a50ede4aeed86dbef4776e3b73347d31d6ad0bc9648ba36049"}, + {file = "regex-2020.10.15.tar.gz", hash = "sha256:d25f5cca0f3af6d425c9496953445bf5b288bb5b71afc2b8308ad194b714c159"}, +] +reorder-python-imports = [ + {file = "reorder_python_imports-2.3.6-py2.py3-none-any.whl", hash = "sha256:1299c31adf341eba2c17543e3cc7fab7389766e726ab54d20440917b5b96b5c5"}, + {file = "reorder_python_imports-2.3.6.tar.gz", hash = "sha256:2ea16d2253536e7f90427b383cd046e46977ca25aae82464883eee882bc7d21b"}, +] +requests = [ + {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, + {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, +] +restructuredtext-lint = [ + {file = "restructuredtext_lint-1.3.1.tar.gz", hash = "sha256:470e53b64817211a42805c3a104d2216f6f5834b22fe7adb637d1de4d6501fb8"}, +] +"ruamel.yaml" = [ + {file = "ruamel.yaml-0.16.12-py2.py3-none-any.whl", hash = "sha256:012b9470a0ea06e4e44e99e7920277edf6b46eee0232a04487ea73a7386340a5"}, + {file = "ruamel.yaml-0.16.12.tar.gz", hash = "sha256:076cc0bc34f1966d920a49f18b52b6ad559fbe656a0748e3535cf7b3f29ebf9e"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-win32.whl", hash = "sha256:30dca9bbcbb1cc858717438218d11eafb78666759e5094dd767468c0d577a7e7"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-win_amd64.whl", hash = "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73b3d43e04cc4b228fa6fa5d796409ece6fcb53a6c270eb2048109cbcbc3b9c2"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:53b9dd1abd70e257a6e32f934ebc482dac5edb8c93e23deb663eac724c30b026"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:839dd72545ef7ba78fd2aa1a5dd07b33696adf3e68fae7f31327161c1093001b"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1236df55e0f73cd138c0eca074ee086136c3f16a97c2ac719032c050f7e0622f"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win32.whl", hash = "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e52c96ca66de04be42ea2278012a2342d89f5e82b4512fb6fb7134e377e2e62"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2fd336a5c6415c82e2deb40d08c222087febe0aebe520f4d21910629018ab0f3"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win32.whl", hash = "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:2602e91bd5c1b874d6f93d3086f9830f3e907c543c7672cf293a97c3fabdcd91"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44c7b0498c39f27795224438f1a6be6c5352f82cb887bc33d962c3a3acc00df6"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8e8fd0a22c9d92af3a34f91e8a2594eeb35cba90ab643c5e0e643567dc8be43e"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:75f0ee6839532e52a3a53f80ce64925ed4aed697dd3fa890c4c918f3304bd4f4"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win32.whl", hash = "sha256:464e66a04e740d754170be5e740657a3b3b6d2bcc567f0c3437879a6e6087ff6"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:52ae5739e4b5d6317b52f5b040b1b6639e8af68a5b8fd606a8b08658fbd0cab5"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df5019e7783d14b79217ad9c56edf1ba7485d614ad5a385d1b3c768635c81c0"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5254af7d8bdf4d5484c089f929cb7f5bafa59b4f01d4f48adda4be41e6d29f99"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8be05be57dc5c7b4a0b24edcaa2f7275866d9c907725226cdde46da09367d923"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win32.whl", hash = "sha256:74161d827407f4db9072011adcfb825b5258a5ccb3d2cd518dd6c9edea9e30f1"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:058a1cc3df2a8aecc12f983a48bda99315cebf55a3b3a5463e37bb599b05727b"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1f8c0a4577c0e6c99d208de5c4d3fd8aceed9574bb154d7a2b21c16bb924154c"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win32.whl", hash = "sha256:46d6d20815064e8bb023ea8628cfb7402c0f0e83de2c2227a88097e239a7dffd"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6c0a5dc52fc74eb87c67374a4e554d4761fd42a4d01390b7e868b30d21f4b8bb"}, + {file = "ruamel.yaml.clib-0.2.2.tar.gz", hash = "sha256:2d24bd98af676f4990c4d715bcdc2a60b19c56a3fb3a763164d2d8ca0e806ba7"}, +] +safety = [ + {file = "safety-1.9.0-py2.py3-none-any.whl", hash = "sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087"}, + {file = "safety-1.9.0.tar.gz", hash = "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +smmap = [ + {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, + {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, +] +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-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"}, + {file = "Sphinx-3.3.1.tar.gz", hash = "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300"}, +] +sphinx-autobuild = [ + {file = "sphinx-autobuild-2020.9.1.tar.gz", hash = "sha256:4b184a7db893f2100bbd831991ae54ca89167a2b9ce68faea71eaa9e37716aed"}, + {file = "sphinx_autobuild-2020.9.1-py3-none-any.whl", hash = "sha256:df5c72cb8b8fc9b31279c4619780c4e95029be6de569ff60a8bb2e99d20f63dd"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-0.5.0-py2.py3-none-any.whl", hash = "sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82"}, + {file = "sphinx_rtd_theme-0.5.0.tar.gz", hash = "sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d"}, +] +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"}, +] +stevedore = [ + {file = "stevedore-3.2.2-py3-none-any.whl", hash = "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62"}, + {file = "stevedore-3.2.2.tar.gz", hash = "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +tornado = [ + {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, + {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, + {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, + {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, + {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, + {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, + {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, + {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, + {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, +] +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-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, + {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-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, + {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-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, + {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-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, + {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, + {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typeguard = [ + {file = "typeguard-2.10.0-py3-none-any.whl", hash = "sha256:a75c6d86ac9d1faf85c5ae952de473e5d26824dda6d4394ff6bc676849cfb939"}, + {file = "typeguard-2.10.0.tar.gz", hash = "sha256:d830132dcd544d3f8a2a842ea739eaa0d7c099fcebb9dcdf3802f4c9929d8191"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +urllib3 = [ + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, +] +virtualenv = [ + {file = "virtualenv-20.0.35-py2.py3-none-any.whl", hash = "sha256:0ebc633426d7468664067309842c81edab11ae97fcaf27e8ad7f5748c89b431b"}, + {file = "virtualenv-20.0.35.tar.gz", hash = "sha256:2a72c80fa2ad8f4e2985c06e6fc12c3d60d060e410572f553c90619b0f6efaf3"}, +] +xdoctest = [ + {file = "xdoctest-0.15.0-py2.py3-none-any.whl", hash = "sha256:695ea04303a48cbb319709270d43f7bae7f3de3701aec73f09d90a216499992e"}, + {file = "xdoctest-0.15.0.tar.gz", hash = "sha256:7f0a184d403b69b166ebec1aadb13c98c96c59101e974ae2e4db4c3a803ec371"}, +] +zipp = [ + {file = "zipp-3.3.1-py3-none-any.whl", hash = "sha256:16522f69653f0d67be90e8baa4a46d66389145b734345d68a257da53df670903"}, + {file = "zipp-3.3.1.tar.gz", hash = "sha256:c1532a8030c32fd52ff6a288d855fe7adef5823ba1d26a29a68fd6314aa72baa"}, +] diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 8fcef624..00000000 --- a/pylintrc +++ /dev/null @@ -1,43 +0,0 @@ -[MASTER] -# Use a conservative default here; 2 should speed up most setups and not hurt -# any too bad. Override on command line as appropriate. -jobs=2 -persistent=no -extension-pkg-whitelist=ciso8601 - -[BASIC] -good-names=id,i,j,k - -[MESSAGES CONTROL] -# Reasons disabled: -# format - handled by black -# duplicate-code - unavoidable -# too-many-* - are not enforced for the sake of readability -# too-few-* - same as too-many-* -# inconsistent-return-statements - doesn't handle raise -# unnecessary-pass - readability for functions which only contain pass -# useless-object-inheritance - should be removed while droping Python 2 -# wrong-import-order - isort guards this -disable= - format, - duplicate-code, - inconsistent-return-statements, - too-few-public-methods, - too-many-ancestors, - too-many-arguments, - too-many-branches, - too-many-instance-attributes, - too-many-lines, - too-many-locals, - too-many-public-methods, - too-many-return-statements, - too-many-statements, - too-many-boolean-expressions, - unnecessary-pass, - useless-object-inheritance, - -[FORMAT] -expected-line-ending-format=LF - -[EXCEPTIONS] -overgeneral-exceptions=PyiCloudException diff --git a/pyproject.toml b/pyproject.toml index 2dfcc69e..bf7603d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,76 @@ -[tool.black] -line-length = 88 -target-version = ["py27", "py33", "py34", "py35", "py36", "py37", "py38"] -exclude = ''' - -( - /( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ - | exceptions.py -) -''' +[tool.poetry] +name = "synologydsm-api" +version = "1.0.1" +description = "Python API for communication with Synology DSM" +authors = ["Quentin POLLET (Quentame)", "FG van Zeelst (ProtoThis)"] +license = "MIT" +readme = "README.rst" +homepage = "https://github.com/hacf-fr/synologydsm-api" +repository = "https://github.com/hacf-fr/synologydsm-api" +documentation = "https://synologydsm-api.readthedocs.io" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Software Development :: Libraries", +] +keywords=["synology-dsm", "synology"] +packages = [ + { include = "synology_dsm", from = "src" }, +] + +[tool.poetry.urls] +Changelog = "https://github.com/hacf-fr/synologydsm-api/releases" + +[tool.poetry.dependencies] +python = "^3.7.0" +requests = "^2.24.0" +urllib3 = "^1.25.10" + +[tool.poetry.dev-dependencies] +pytest = "^6.1.2" +coverage = {extras = ["toml"], version = "^5.3"} +safety = "^1.9.0" +mypy = "^0.790" +typeguard = "^2.9.1" +xdoctest = {extras = ["colors"], version = "^0.15.0"} +sphinx = "^3.3.1" +sphinx-autobuild = "^2020.9.1" +pre-commit = "^2.8.2" +flake8 = "^3.8.4" +black = "^20.8b1" +flake8-bandit = "^2.1.2" +flake8-bugbear = "^20.1.4" +flake8-docstrings = "^1.5.0" +flake8-rst-docstrings = "^0.0.14" +pep8-naming = "^0.11.1" +darglint = "^1.5.5" +reorder-python-imports = "^2.3.6" +pre-commit-hooks = "^3.3.0" +sphinx-rtd-theme = "^0.5.0" +Pygments = "^2.7.2" + +[tool.poetry.scripts] +synologydsm-api = "synology_dsm.__main__:main" + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.coverage.run] +branch = true +source = ["synology_dsm"] + +[tool.coverage.report] +show_missing = true +fail_under = 80 + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ca3f09a8..00000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests>=2.24.0 -# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 -urllib3>=1.24.3 diff --git a/requirements_all.txt b/requirements_all.txt deleted file mode 100644 index daefbd08..00000000 --- a/requirements_all.txt +++ /dev/null @@ -1,2 +0,0 @@ --r requirements.txt --r requirements_test.txt diff --git a/requirements_test.txt b/requirements_test.txt deleted file mode 100644 index d69693d8..00000000 --- a/requirements_test.txt +++ /dev/null @@ -1,4 +0,0 @@ -pytest -pylint>=2.6.0 -pylint-strict-informational==0.1 -black==20.8b1 diff --git a/scripts/clean.sh b/scripts/clean.sh index 4c4325e1..f5ee09e8 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -5,3 +5,4 @@ rm -r .tox rm -r build rm -r dist rm -r python_synology.egg-info +rm -r src/python_synology.egg-info diff --git a/scripts/publish.sh b/scripts/publish.sh index c0f3ff51..1b9b2754 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,5 +1,5 @@ # Publish the library -# https://pypi.org/project/python-synology +# https://pypi.org/project/synologydsm-api # Publish documentation here: https://packaging.python.org/tutorials/packaging-projects/ ./scripts/common.sh @@ -14,5 +14,6 @@ python3 setup.py sdist bdist_wheel # Push to PyPi python3 -m twine upload dist/* +# python3 -m twine upload --repository testpypi dist/* # Enter credentials manually :P diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b7e88a6a..00000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[tool:pytest] -testpaths = tests -norecursedirs=.git .tox build lib diff --git a/setup.py b/setup.py deleted file mode 100644 index 5f5a3411..00000000 --- a/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python -"""Synology DSM setup.""" - -# NOTE(ProtoThis) Guidelines for Major.Minor.Micro -# - Major means an API contract change -# - Minor means API bugfix or new functionality -# - Micro means change of any kind (unless significant enough for a minor/major). - -from setuptools import setup, find_packages - -REPO_URL = "https://github.com/ProtoThis/python-synology" -VERSION = "0.9.0" - -with open("requirements.txt") as f: - required = f.read().splitlines() - -with open("README.rst", encoding="utf-8") as f: - long_description = f.read() - -setup( - name="python-synology", - version=VERSION, - url=REPO_URL, - download_url=REPO_URL + "/tarball/" + VERSION, - description="Python API for communication with Synology DSM", - long_description=long_description, - author="Quentin POLLET (Quentame) & FG van Zeelst (ProtoThis)", - packages=find_packages(include=["synology_dsm*"]), - install_requires=required, - python_requires=">=3.6", - license="MIT", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Topic :: Software Development :: Libraries", - ], - keywords=["synology-dsm", "synology"], -) diff --git a/src/synology_dsm/__init__.py b/src/synology_dsm/__init__.py new file mode 100644 index 00000000..f453784e --- /dev/null +++ b/src/synology_dsm/__init__.py @@ -0,0 +1,4 @@ +"""The synologydsm-api library.""" +from .synology_dsm import SynologyDSM + +__all__ = ["SynologyDSM"] diff --git a/synology_dsm/api/__init__.py b/src/synology_dsm/api/__init__.py similarity index 100% rename from synology_dsm/api/__init__.py rename to src/synology_dsm/api/__init__.py diff --git a/synology_dsm/api/core/__init__.py b/src/synology_dsm/api/core/__init__.py similarity index 100% rename from synology_dsm/api/core/__init__.py rename to src/synology_dsm/api/core/__init__.py diff --git a/synology_dsm/api/core/security.py b/src/synology_dsm/api/core/security.py similarity index 81% rename from synology_dsm/api/core/security.py rename to src/synology_dsm/api/core/security.py index fcc074c1..eed4ff54 100644 --- a/synology_dsm/api/core/security.py +++ b/src/synology_dsm/api/core/security.py @@ -7,6 +7,7 @@ class SynoCoreSecurity: API_KEY = "SYNO.Core.SecurityScan.Status" def __init__(self, dsm): + """Constructor method.""" self._dsm = dsm self._data = {} @@ -38,12 +39,18 @@ def success(self): @property def progress(self): - """Gets the scan progress (100 if finished).""" + """Gets the scan progress. + + Returns: 100 if finished + """ return self._data.get("sysProgress") @property def status(self): - """Gets the last scan status (safe, danger, info, outOfDate, risk, warning).""" + """Gets the last scan status. + + Possible values: safe, danger, info, outOfDate, risk, warning. + """ return self._data.get("sysStatus") @property diff --git a/synology_dsm/api/core/share.py b/src/synology_dsm/api/core/share.py similarity index 84% rename from synology_dsm/api/core/share.py rename to src/synology_dsm/api/core/share.py index bcc5ad56..aa6d2336 100644 --- a/synology_dsm/api/core/share.py +++ b/src/synology_dsm/api/core/share.py @@ -11,15 +11,17 @@ class SynoCoreShare: # are returned plus any keys listed in the "additional" parameter. # NOTE: The value of the additional key must be a string. REQUEST_DATA = { - "additional": '["hidden","encryption","is_aclmode","unite_permission","is_support_acl",' - '"is_sync_share","is_force_readonly","force_readonly_reason","recyclebin",' - '"is_share_moving","is_cluster_share","is_exfat_share","is_cold_storage_share",' - '"support_snapshot","share_quota","enable_share_compress","enable_share_cow",' - '"include_cold_storage_share","is_cold_storage_share"]', + "additional": '["hidden","encryption","is_aclmode","unite_permission",' + '"is_support_acl","is_sync_share","is_force_readonly","force_readonly_reason",' + '"recyclebin","is_share_moving","is_cluster_share","is_exfat_share",' + '"is_cold_storage_share","support_snapshot","share_quota",' + '"enable_share_compress","enable_share_cow","include_cold_storage_share",' + '"is_cold_storage_share"]', "shareType": "all", } def __init__(self, dsm): + """Constructor method.""" self._dsm = dsm self._data = {} diff --git a/synology_dsm/api/core/system.py b/src/synology_dsm/api/core/system.py similarity index 64% rename from synology_dsm/api/core/system.py rename to src/synology_dsm/api/core/system.py index af9faa51..4e455b71 100644 --- a/synology_dsm/api/core/system.py +++ b/src/synology_dsm/api/core/system.py @@ -7,6 +7,7 @@ class SynoCoreSystem: API_KEY = "SYNO.Core.System" def __init__(self, dsm): + """Constructor method.""" self._dsm = dsm self._data = {} @@ -16,94 +17,98 @@ def update(self): if raw_data: self._data = raw_data["data"] - ### get information + # + # get information + # @property def cpu_clock_speed(self): """Gets System CPU clock speed.""" - return self._data.get('cpu_clock_speed') + return self._data.get("cpu_clock_speed") @property def cpu_cores(self): """Gets System CPU cores.""" - return self._data.get('cpu_cores') + return self._data.get("cpu_cores") @property def cpu_family(self): """Gets System CPU family.""" - return self._data.get('cpu_family') + return self._data.get("cpu_family") @property def cpu_series(self): """Gets System CPU series.""" - return self._data.get('cpu_series') + return self._data.get("cpu_series") @property def enabled_ntp(self): """Gets System NTP state.""" - return self._data.get('enabled_ntp') + return self._data.get("enabled_ntp") @property def ntp_server(self): """Gets System NTP server.""" - return self._data.get('ntp_server') + return self._data.get("ntp_server") @property def firmware_ver(self): """Gets System firmware version.""" - return self._data.get('firmware_ver') + return self._data.get("firmware_ver") @property def model(self): """Gets System model.""" - return self._data.get('model') + return self._data.get("model") @property def ram_size(self): """Gets System ram size.""" - return self._data.get('ram_size') + return self._data.get("ram_size") @property def serial(self): """Gets System serial number.""" - return self._data.get('serial') + return self._data.get("serial") @property def sys_temp(self): """Gets System temperature.""" - return self._data.get('sys_temp') + return self._data.get("sys_temp") @property def time(self): """Gets System time.""" - return self._data.get('time') + return self._data.get("time") @property def time_zone(self): """Gets System time zone.""" - return self._data.get('time_zone') + return self._data.get("time_zone") @property def time_zone_desc(self): """Gets System time zone description.""" - return self._data.get('time_zone_desc') + return self._data.get("time_zone_desc") @property def up_time(self): """Gets System uptime.""" - return self._data.get('up_time') + return self._data.get("up_time") @property def usb_dev(self): """Gets System connected usb devices.""" - return self._data.get('usb_dev', []) + return self._data.get("usb_dev", []) - ### do system actions + # + # do system actions + # def shutdown(self): """Shutdown NAS.""" res = self._dsm.get( self.API_KEY, "shutdown", - max_version=1, # shutdown method is only available on api version 1 + max_version=1, # shutdown method is only available on api version 1 ) return res @@ -112,6 +117,6 @@ def reboot(self): res = self._dsm.get( self.API_KEY, "reboot", - max_version=1, # reboot method is only available on api version 1 + max_version=1, # reboot method is only available on api version 1 ) return res diff --git a/src/synology_dsm/api/core/upgrade.py b/src/synology_dsm/api/core/upgrade.py new file mode 100644 index 00000000..e491ff8f --- /dev/null +++ b/src/synology_dsm/api/core/upgrade.py @@ -0,0 +1,39 @@ +"""DSM Upgrade data and actions.""" + + +class SynoCoreUpgrade: + """Class containing upgrade data and actions.""" + + API_KEY = "SYNO.Core.Upgrade" + API_SERVER_KEY = API_KEY + ".Server" + + def __init__(self, dsm): + """Constructor method.""" + self._dsm = dsm + self._data = {} + + def update(self): + """Updates Upgrade data.""" + raw_data = self._dsm.get(self.API_SERVER_KEY, "check") + if raw_data: + self._data = raw_data["data"].get("update", raw_data["data"]) + + @property + def update_available(self): + """Gets available update info.""" + return self._data.get("available") + + @property + def available_version(self): + """Gets available verion info.""" + return self._data.get("version") + + @property + def reboot_needed(self): + """Gets info if reboot is needed.""" + return self._data.get("reboot") + + @property + def service_restarts(self): + """Gets info if services are restarted.""" + return self._data.get("restart") diff --git a/synology_dsm/api/core/utilization.py b/src/synology_dsm/api/core/utilization.py similarity index 93% rename from synology_dsm/api/core/utilization.py rename to src/synology_dsm/api/core/utilization.py index e768c51e..d25946b4 100644 --- a/synology_dsm/api/core/utilization.py +++ b/src/synology_dsm/api/core/utilization.py @@ -8,6 +8,7 @@ class SynoCoreUtilization: API_KEY = "SYNO.Core.System.Utilization" def __init__(self, dsm): + """Constructor method.""" self._dsm = dsm self._data = {} @@ -24,17 +25,17 @@ def cpu(self): @property def cpu_other_load(self): - """'Other' percentage of the total CPU load.""" + """Other percentage of the total CPU load.""" return self.cpu.get("other_load") @property def cpu_user_load(self): - """'User' percentage of the total CPU load.""" + """User percentage of the total CPU load.""" return self.cpu.get("user_load") @property def cpu_system_load(self): - """'System' percentage of the total CPU load.""" + """System percentage of the total CPU load.""" return self.cpu.get("system_load") @property diff --git a/synology_dsm/api/download_station/__init__.py b/src/synology_dsm/api/download_station/__init__.py similarity index 100% rename from synology_dsm/api/download_station/__init__.py rename to src/synology_dsm/api/download_station/__init__.py diff --git a/synology_dsm/api/download_station/task.py b/src/synology_dsm/api/download_station/task.py similarity index 85% rename from synology_dsm/api/download_station/task.py rename to src/synology_dsm/api/download_station/task.py index b4b6a6cb..b3747ec9 100644 --- a/synology_dsm/api/download_station/task.py +++ b/src/synology_dsm/api/download_station/task.py @@ -39,7 +39,11 @@ def size(self): @property def status(self): - """Return status of the task (waiting, downloading, paused, finishing, finished, hash_checking, seeding, filehosting_waiting, extracting, error).""" + """Return status of the task. + + Possible values: waiting, downloading, paused, finishing, finished, + hash_checking, seeding, filehosting_waiting, extracting, error + """ return self._data["status"] @property diff --git a/synology_dsm/api/dsm/__init__.py b/src/synology_dsm/api/dsm/__init__.py similarity index 100% rename from synology_dsm/api/dsm/__init__.py rename to src/synology_dsm/api/dsm/__init__.py diff --git a/synology_dsm/api/dsm/information.py b/src/synology_dsm/api/dsm/information.py similarity index 93% rename from synology_dsm/api/dsm/information.py rename to src/synology_dsm/api/dsm/information.py index 7c73cc9e..709a07fb 100644 --- a/synology_dsm/api/dsm/information.py +++ b/src/synology_dsm/api/dsm/information.py @@ -7,6 +7,7 @@ class SynoDSMInformation: API_KEY = "SYNO.DSM.Info" def __init__(self, dsm): + """Constructor methods.""" self._dsm = dsm self._data = {} diff --git a/synology_dsm/api/dsm/network.py b/src/synology_dsm/api/dsm/network.py similarity index 85% rename from synology_dsm/api/dsm/network.py rename to src/synology_dsm/api/dsm/network.py index 451d111c..b4195894 100644 --- a/synology_dsm/api/dsm/network.py +++ b/src/synology_dsm/api/dsm/network.py @@ -7,6 +7,7 @@ class SynoDSMNetwork: API_KEY = "SYNO.DSM.Network" def __init__(self, dsm): + """Constructor method.""" self._dsm = dsm self._data = {} @@ -45,10 +46,11 @@ def interface(self, eth_id): @property def macs(self): - """MACs of the NAS.""" + """MACs of the NAS.""" # noqa: D403 macs = [] for interface in self.interfaces: - macs.append(interface["mac"]) + if interface.get("mac"): + macs.append(interface["mac"]) return macs @property diff --git a/synology_dsm/api/storage/__init__.py b/src/synology_dsm/api/storage/__init__.py similarity index 100% rename from synology_dsm/api/storage/__init__.py rename to src/synology_dsm/api/storage/__init__.py diff --git a/synology_dsm/api/storage/storage.py b/src/synology_dsm/api/storage/storage.py similarity index 96% rename from synology_dsm/api/storage/storage.py rename to src/synology_dsm/api/storage/storage.py index ed2ce661..11c2409e 100644 --- a/synology_dsm/api/storage/storage.py +++ b/src/synology_dsm/api/storage/storage.py @@ -1,5 +1,4 @@ """DSM Storage data.""" - from synology_dsm.helpers import SynoFormatHelper @@ -9,6 +8,7 @@ class SynoStorage: API_KEY = "SYNO.Storage.CGI.Storage" def __init__(self, dsm): + """Constructor method.""" self._dsm = dsm self._data = {} diff --git a/synology_dsm/api/surveillance_station/__init__.py b/src/synology_dsm/api/surveillance_station/__init__.py similarity index 89% rename from synology_dsm/api/surveillance_station/__init__.py rename to src/synology_dsm/api/surveillance_station/__init__.py index db0823f8..001ab6cd 100644 --- a/synology_dsm/api/surveillance_station/__init__.py +++ b/src/synology_dsm/api/surveillance_station/__init__.py @@ -1,8 +1,7 @@ """Synology SurveillanceStation API wrapper.""" -import urllib - from .camera import SynoCamera -from .const import MOTION_DETECTION_BY_SURVEILLANCE, MOTION_DETECTION_DISABLED +from .const import MOTION_DETECTION_BY_SURVEILLANCE +from .const import MOTION_DETECTION_DISABLED class SynoSurveillanceStation: @@ -63,7 +62,12 @@ def get_camera(self, camera_id): return self._cameras_by_id[camera_id] def get_camera_live_view_path(self, camera_id, video_format=None): - """Return camera live view path matching camera_id (video_format: mjpeg_http | multicast | mxpeg_http | rtsp_http | rtsp).""" + """Return camera live view path matching camera_id. + + Args: + camera_id: ID of the camera we want to get the live view path. + video_format: mjpeg_http | multicast | mxpeg_http | rtsp_http | rtsp. + """ if video_format: return getattr(self._cameras_by_id[camera_id].live_view, video_format) return self._cameras_by_id[camera_id].live_view @@ -99,7 +103,12 @@ def capture_camera_image(self, camera_id, save=True): ) def download_snapshot(self, snapshot_id, snapshot_size): - """Download snapshot image binary for a givent snapshot_id (snapshot_size: SNAPSHOT_SIZE_ICON | SNAPSHOT_SIZE_FULL).""" + """Download snapshot image binary for a givent snapshot_id. + + Args: + snapshot_id: ID of the snapshot we want to download. + snapshot_size: SNAPSHOT_SIZE_ICON | SNAPSHOT_SIZE_FULL. + """ return self._dsm.get( self.SNAPSHOT_API_KEY, "LoadSnapshot", @@ -129,11 +138,11 @@ def disable_motion_detection(self, camera_id): # Home mode def get_home_mode_status(self): - """Get the state of Home Mode""" + """Get the state of Home Mode.""" return self._dsm.get(self.HOME_MODE_API_KEY, "GetInfo")["data"]["on"] def set_home_mode(self, state): - """Set the state of Home Mode (state: bool)""" + """Set the state of Home Mode (state: bool).""" return self._dsm.get( self.HOME_MODE_API_KEY, "Switch", {"on": str(state).lower()} )["success"] diff --git a/synology_dsm/api/surveillance_station/camera.py b/src/synology_dsm/api/surveillance_station/camera.py similarity index 97% rename from synology_dsm/api/surveillance_station/camera.py rename to src/synology_dsm/api/surveillance_station/camera.py index 0c32812e..c3859ace 100644 --- a/synology_dsm/api/surveillance_station/camera.py +++ b/src/synology_dsm/api/surveillance_station/camera.py @@ -1,5 +1,6 @@ """SurveillanceStation camera.""" -from .const import RECORDING_STATUS, MOTION_DETECTION_DISABLED +from .const import MOTION_DETECTION_DISABLED +from .const import RECORDING_STATUS class SynoCamera: diff --git a/synology_dsm/api/surveillance_station/const.py b/src/synology_dsm/api/surveillance_station/const.py similarity index 100% rename from synology_dsm/api/surveillance_station/const.py rename to src/synology_dsm/api/surveillance_station/const.py diff --git a/synology_dsm/const.py b/src/synology_dsm/const.py similarity index 97% rename from synology_dsm/const.py rename to src/synology_dsm/const.py index 9d4609b9..0674adfd 100644 --- a/synology_dsm/const.py +++ b/src/synology_dsm/const.py @@ -1,5 +1,4 @@ """Library constants.""" - # APIs API_INFO = "SYNO.API.Info" API_AUTH = "SYNO.API.Auth" diff --git a/synology_dsm/exceptions.py b/src/synology_dsm/exceptions.py similarity index 79% rename from synology_dsm/exceptions.py rename to src/synology_dsm/exceptions.py index 799fdc99..b05ab887 100644 --- a/synology_dsm/exceptions.py +++ b/src/synology_dsm/exceptions.py @@ -1,9 +1,19 @@ """Library exceptions.""" -from .const import API_AUTH, ERROR_AUTH, ERROR_COMMON, ERROR_DOWNLOAD_SEARCH, ERROR_DOWNLOAD_TASK, ERROR_FILE, ERROR_SURVEILLANCE, ERROR_VIRTUALIZATION +from .const import API_AUTH +from .const import ERROR_AUTH +from .const import ERROR_COMMON +from .const import ERROR_DOWNLOAD_SEARCH +from .const import ERROR_DOWNLOAD_TASK +from .const import ERROR_FILE +from .const import ERROR_SURVEILLANCE +from .const import ERROR_VIRTUALIZATION + class SynologyDSMException(Exception): """Generic Synology DSM exception.""" + def __init__(self, api, code, details=None): + """Constructor method.""" reason = ERROR_COMMON.get(code) if api and not reason: if api == API_AUTH: @@ -21,68 +31,90 @@ def __init__(self, api, code, details=None): reason = ERROR_VIRTUALIZATION.get(code) if not reason: reason = "Unknown" - - error_message={"api": api, "code": code, "reason": reason, "details": details} + + error_message = {"api": api, "code": code, "reason": reason, "details": details} super().__init__(error_message) + # Request class SynologyDSMRequestException(SynologyDSMException): """Request exception.""" + def __init__(self, exception): + """Constructor method.""" ex_class = exception.__class__.__name__ ex_reason = exception.args[0] if hasattr(exception.args[0], "reason"): ex_reason = exception.args[0].reason super().__init__(None, -1, f"{ex_class} = {ex_reason}") + # API class SynologyDSMAPINotExistsException(SynologyDSMException): """API not exists exception.""" + def __init__(self, api): + """Constructor method.""" super().__init__(api, -2, f"API {api} does not exists") + class SynologyDSMAPIErrorException(SynologyDSMException): """API returns an error exception.""" + def __init__(self, api, code, details): + """Constructor method.""" super().__init__(api, code, details) + # Login class SynologyDSMLoginFailedException(SynologyDSMException): """Failed to login exception.""" + def __init__(self, code, details=None): + """Constructor method.""" super().__init__(API_AUTH, code, details) class SynologyDSMLoginInvalidException(SynologyDSMLoginFailedException): """Invalid password & not admin account exception.""" + def __init__(self, username): + """Constructor method.""" message = f"Invalid password or not admin account: {username}" super().__init__(400, message) class SynologyDSMLoginDisabledAccountException(SynologyDSMLoginFailedException): """Guest & disabled account exception.""" + def __init__(self, username): + """Constructor method.""" message = f"Guest or disabled account: {username}" super().__init__(401, message) class SynologyDSMLoginPermissionDeniedException(SynologyDSMLoginFailedException): """No access to login exception.""" + def __init__(self, username): + """Constructor method.""" message = f"Permission denied for account: {username}" super().__init__(402, message) class SynologyDSMLogin2SARequiredException(SynologyDSMLoginFailedException): """2SA required to login exception.""" + def __init__(self, username): + """Constructor method.""" message = f"Two-step authentication required for account: {username}" super().__init__(403, message) class SynologyDSMLogin2SAFailedException(SynologyDSMLoginFailedException): """2SA code failed exception.""" + def __init__(self): + """Constructor method.""" message = "Two-step authentication failed, retry with a new pass code" super().__init__(404, message) diff --git a/synology_dsm/helpers.py b/src/synology_dsm/helpers.py similarity index 92% rename from synology_dsm/helpers.py rename to src/synology_dsm/helpers.py index 8098771e..5a120de2 100644 --- a/synology_dsm/helpers.py +++ b/src/synology_dsm/helpers.py @@ -7,7 +7,7 @@ class SynoFormatHelper: @staticmethod def bytes_to_readable(num): """Converts bytes to a human readable format.""" - if num < 512: # pylint: disable=no-else-return + if num < 512: return "0 Kb" elif num < 1024: return "1 Kb" diff --git a/synology_dsm/synology_dsm.py b/src/synology_dsm/synology_dsm.py similarity index 85% rename from synology_dsm/synology_dsm.py rename to src/synology_dsm/synology_dsm.py index 87f09d54..6b6a09eb 100644 --- a/synology_dsm/synology_dsm.py +++ b/src/synology_dsm/synology_dsm.py @@ -1,24 +1,12 @@ """Class to interact with Synology DSM.""" +import socket from json import JSONDecodeError from urllib.parse import quote -import socket import urllib3 from requests import Session from requests.exceptions import RequestException -from .exceptions import ( - SynologyDSMAPIErrorException, - SynologyDSMAPINotExistsException, - SynologyDSMRequestException, - SynologyDSMLoginFailedException, - SynologyDSMLoginInvalidException, - SynologyDSMLoginDisabledAccountException, - SynologyDSMLoginPermissionDeniedException, - SynologyDSMLogin2SARequiredException, - SynologyDSMLogin2SAFailedException, -) - from .api.core.security import SynoCoreSecurity from .api.core.share import SynoCoreShare from .api.core.system import SynoCoreSystem @@ -29,7 +17,17 @@ from .api.dsm.network import SynoDSMNetwork from .api.storage.storage import SynoStorage from .api.surveillance_station import SynoSurveillanceStation -from .const import API_AUTH, API_INFO +from .const import API_AUTH +from .const import API_INFO +from .exceptions import SynologyDSMAPIErrorException +from .exceptions import SynologyDSMAPINotExistsException +from .exceptions import SynologyDSMLogin2SAFailedException +from .exceptions import SynologyDSMLogin2SARequiredException +from .exceptions import SynologyDSMLoginDisabledAccountException +from .exceptions import SynologyDSMLoginFailedException +from .exceptions import SynologyDSMLoginInvalidException +from .exceptions import SynologyDSMLoginPermissionDeniedException +from .exceptions import SynologyDSMRequestException class SynologyDSM: @@ -46,18 +44,21 @@ def __init__( username: str, password: str, use_https: bool = False, + verify_ssl: bool = False, timeout: int = None, device_token: str = None, debugmode: bool = False, ): + """Constructor method.""" self.username = username self._password = password self._timeout = timeout or 10 self._debugmode = debugmode + self._verify = verify_ssl & use_https # Session self._session = Session() - self._session.verify = False + self._session.verify = self._verify # Login self._session_id = None @@ -81,9 +82,10 @@ def __init__( # Build variables if use_https: - # https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings - # disable SSL warnings due to the auto-genenerated cert - urllib3.disable_warnings() + if not verify_ssl: + # https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings # noqa: B950 + # disable SSL warnings due to the auto-genenerated cert + urllib3.disable_warnings() self._base_url = f"https://{dsm_ip}:{dsm_port}" else: @@ -95,7 +97,11 @@ def _debuglog(self, message: str): print("DEBUG: " + message) def _is_weird_api_url(self, api: str) -> bool: - """Returns True if the API URL is not common (nas_base_url/webapi/path?params) [Only handles DSM 5 for now].""" + """Returns True if the API URL is not common. + + Common template is nas_base_url/webapi/path?params + Only handles DSM 5 for now. + """ return ( api in self.DSM_5_WEIRD_URL_API and self._information @@ -106,7 +112,10 @@ def _is_weird_api_url(self, api: str) -> bool: def _build_url(self, api: str) -> str: if self._is_weird_api_url(api): if api == SynoStorage.API_KEY: - return f"{self._base_url}/webman/modules/StorageManager/storagehandler.cgi?" + return ( + f"{self._base_url}/webman/modules/StorageManager/" + f"storagehandler.cgi?" + ) return f"{self._base_url}/webapi/{self.apis[api]['path']}?" @@ -121,12 +130,12 @@ def apis(self): """Gets available API infos from the NAS.""" return self._apis - def login(self, otp_code: str = None): + def login(self, otp_code: str = None) -> bool: """Create a logged session.""" # First reset the session self._debuglog("Creating new session") self._session = Session() - self._session.verify = False + self._session.verify = self._verify params = { "account": self.username, @@ -165,7 +174,8 @@ def login(self, otp_code: str = None): # Not available on API version < 3 self._syno_token = result["data"]["synotoken"] if result["data"].get("did"): - # Not available on API version < 6 && device token is given once per device_name + # Not available on API version < 6 && device token is given once + # per device_name self._device_token = result["data"]["did"] self._debuglog("Authentication successful, token: " + str(self._session_id)) @@ -173,11 +183,20 @@ def login(self, otp_code: str = None): self._information = SynoDSMInformation(self) self._information.update() - return True + return result["success"] + + def logout(self) -> bool: + """Log out of the session.""" + result = self.get(API_AUTH, "logout") + self._session = None + return result["success"] @property def device_token(self) -> str: - """Gets the device token to remember the 2SA access was granted on this device.""" + """Gets the device token. + + Used to remember the 2SA access was granted on this device. + """ return self._device_token def get(self, api: str, method: str, params: dict = None, **kwargs): @@ -195,7 +214,7 @@ def _request( method: str, params: dict = None, retry_once: bool = True, - **kwargs + **kwargs, ): """Handles API request.""" # Discover existing APIs @@ -243,7 +262,8 @@ def _request( if isinstance(response, dict) and response.get("error") and api != API_AUTH: self._debuglog("Session error: " + str(response["error"]["code"])) if response["error"]["code"] == 119 and retry_once: - # Session ID not valid, see https://github.com/aerialls/synology-srm/pull/3 + # Session ID not valid + # see https://github.com/aerialls/synology-srm/pull/3 self._session_id = None self._syno_token = None self._device_token = None diff --git a/synology_dsm/__init__.py b/synology_dsm/__init__.py deleted file mode 100644 index 092f3fcb..00000000 --- a/synology_dsm/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""The python-synology library.""" -from .synology_dsm import SynologyDSM diff --git a/synology_dsm/api/core/upgrade.py b/synology_dsm/api/core/upgrade.py deleted file mode 100644 index 838444ee..00000000 --- a/synology_dsm/api/core/upgrade.py +++ /dev/null @@ -1,23 +0,0 @@ -"""DSM Upgrade data and actions.""" - - -class SynoCoreUpgrade: - """Class containing upgrade data and actions.""" - - API_KEY = "SYNO.Core.Upgrade" - API_SERVER_KEY = API_KEY + ".Server" - - def __init__(self, dsm): - self._dsm = dsm - self._data = {} - - def update(self): - """Updates Upgrade data.""" - raw_data = self._dsm.get(self.API_SERVER_KEY, "check") - if raw_data: - self._data = raw_data["data"] - - @property - def update_available(self): - """Gets all Upgrade info.""" - return self._data["update"].get("available") diff --git a/tests/__init__.py b/tests/__init__.py index a7356fbe..b42188ac 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,65 +2,67 @@ from json import JSONDecodeError from urllib.parse import urlencode -from requests.exceptions import ConnectionError as ConnError, RequestException, SSLError - +from requests.exceptions import ConnectionError as ConnError +from requests.exceptions import RequestException +from requests.exceptions import SSLError + +from .api_data.dsm_5 import DSM_5_API_INFO +from .api_data.dsm_5 import DSM_5_AUTH_LOGIN +from .api_data.dsm_5 import DSM_5_AUTH_LOGIN_2SA +from .api_data.dsm_5 import DSM_5_AUTH_LOGIN_2SA_OTP +from .api_data.dsm_5 import DSM_5_CORE_UTILIZATION +from .api_data.dsm_5 import DSM_5_DSM_INFORMATION +from .api_data.dsm_5 import DSM_5_DSM_NETWORK +from .api_data.dsm_5 import DSM_5_STORAGE_STORAGE_DS410J_RAID5_4DISKS_1VOL +from .api_data.dsm_6 import DSM_6_API_INFO +from .api_data.dsm_6 import DSM_6_API_INFO_SURVEILLANCE_STATION +from .api_data.dsm_6 import DSM_6_AUTH_LOGIN +from .api_data.dsm_6 import DSM_6_AUTH_LOGIN_2SA +from .api_data.dsm_6 import DSM_6_AUTH_LOGIN_2SA_OTP +from .api_data.dsm_6 import DSM_6_CORE_SECURITY +from .api_data.dsm_6 import DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE +from .api_data.dsm_6 import DSM_6_CORE_SHARE +from .api_data.dsm_6 import DSM_6_CORE_SYSTEM_DS918_PLUS +from .api_data.dsm_6 import DSM_6_CORE_UPGRADE_TRUE +from .api_data.dsm_6 import DSM_6_CORE_UTILIZATION +from .api_data.dsm_6 import DSM_6_CORE_UTILIZATION_ERROR_1055 +from .api_data.dsm_6 import DSM_6_DOWNLOAD_STATION_INFO_CONFIG +from .api_data.dsm_6 import DSM_6_DOWNLOAD_STATION_INFO_INFO +from .api_data.dsm_6 import DSM_6_DOWNLOAD_STATION_STAT_INFO +from .api_data.dsm_6 import DSM_6_DOWNLOAD_STATION_TASK_LIST +from .api_data.dsm_6 import DSM_6_DSM_INFORMATION +from .api_data.dsm_6 import DSM_6_DSM_NETWORK_2LAN_1PPPOE +from .api_data.dsm_6 import ( + DSM_6_STORAGE_STORAGE_DS1515_PLUS_SHR2_10DISKS_1VOL_WITH_EXPANSION, +) +from .api_data.dsm_6 import DSM_6_STORAGE_STORAGE_DS1819_PLUS_SHR2_8DISKS_1VOL +from .api_data.dsm_6 import DSM_6_STORAGE_STORAGE_DS213_PLUS_SHR1_2DISKS_2VOLS +from .api_data.dsm_6 import DSM_6_STORAGE_STORAGE_DS918_PLUS_RAID5_3DISKS_1VOL +from .api_data.dsm_6 import DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MD_PARAM_SAVE +from .api_data.dsm_6 import DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MOTION_ENUM +from .api_data.dsm_6 import DSM_6_SURVEILLANCE_STATION_CAMERA_GET_LIVE_VIEW_PATH +from .api_data.dsm_6 import DSM_6_SURVEILLANCE_STATION_CAMERA_LIST +from .api_data.dsm_6 import DSM_6_SURVEILLANCE_STATION_HOME_MODE_GET_INFO +from .api_data.dsm_6 import DSM_6_SURVEILLANCE_STATION_HOME_MODE_SWITCH +from .const import DEVICE_TOKEN +from .const import ERROR_AUTH_INVALID_CREDENTIALS +from .const import ERROR_AUTH_MAX_TRIES +from .const import ERROR_AUTH_OTP_AUTHENTICATE_FAILED +from .const import ERROR_INSUFFICIENT_USER_PRIVILEGE from synology_dsm import SynologyDSM -from synology_dsm.exceptions import SynologyDSMRequestException from synology_dsm.api.core.security import SynoCoreSecurity +from synology_dsm.api.core.share import SynoCoreShare +from synology_dsm.api.core.system import SynoCoreSystem +from synology_dsm.api.core.upgrade import SynoCoreUpgrade from synology_dsm.api.core.utilization import SynoCoreUtilization +from synology_dsm.api.download_station import SynoDownloadStation from synology_dsm.api.dsm.information import SynoDSMInformation from synology_dsm.api.dsm.network import SynoDSMNetwork -from synology_dsm.api.download_station import SynoDownloadStation from synology_dsm.api.storage.storage import SynoStorage -from synology_dsm.api.core.share import SynoCoreShare from synology_dsm.api.surveillance_station import SynoSurveillanceStation -from synology_dsm.const import API_AUTH, API_INFO - -from .const import ( - ERROR_INSUFFICIENT_USER_PRIVILEGE, - ERROR_AUTH_INVALID_CREDENTIALS, - ERROR_AUTH_MAX_TRIES, - ERROR_AUTH_OTP_AUTHENTICATE_FAILED, - DEVICE_TOKEN, -) -from .api_data.dsm_6 import ( - DSM_6_API_INFO, - DSM_6_AUTH_LOGIN, - DSM_6_AUTH_LOGIN_2SA, - DSM_6_AUTH_LOGIN_2SA_OTP, - DSM_6_DSM_INFORMATION, - DSM_6_DSM_NETWORK, - DSM_6_CORE_UTILIZATION, - DSM_6_CORE_UTILIZATION_ERROR_1055, - DSM_6_CORE_SECURITY, - DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE, - DSM_6_STORAGE_STORAGE_DS213_PLUS_SHR1_2DISKS_2VOLS, - DSM_6_STORAGE_STORAGE_DS918_PLUS_RAID5_3DISKS_1VOL, - DSM_6_STORAGE_STORAGE_DS1819_PLUS_SHR2_8DISKS_1VOL, - DSM_6_STORAGE_STORAGE_DS1515_PLUS_SHR2_10DISKS_1VOL_WITH_EXPANSION, - DSM_6_CORE_SHARE, - DSM_6_API_INFO_SURVEILLANCE_STATION, - DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MOTION_ENUM, - DSM_6_SURVEILLANCE_STATION_CAMERA_GET_LIVE_VIEW_PATH, - DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MD_PARAM_SAVE, - DSM_6_SURVEILLANCE_STATION_CAMERA_LIST, - DSM_6_SURVEILLANCE_STATION_HOME_MODE_GET_INFO, - DSM_6_SURVEILLANCE_STATION_HOME_MODE_SWITCH, - DSM_6_DOWNLOAD_STATION_INFO_INFO, - DSM_6_DOWNLOAD_STATION_INFO_CONFIG, - DSM_6_DOWNLOAD_STATION_STAT_INFO, - DSM_6_DOWNLOAD_STATION_TASK_LIST, -) -from .api_data.dsm_5 import ( - DSM_5_API_INFO, - DSM_5_AUTH_LOGIN, - DSM_5_AUTH_LOGIN_2SA, - DSM_5_AUTH_LOGIN_2SA_OTP, - DSM_5_DSM_NETWORK, - DSM_5_DSM_INFORMATION, - DSM_5_CORE_UTILIZATION, - DSM_5_STORAGE_STORAGE_DS410J_RAID5_4DISKS_1VOL, -) +from synology_dsm.const import API_AUTH +from synology_dsm.const import API_INFO +from synology_dsm.exceptions import SynologyDSMRequestException API_SWITCHER = { 5: { @@ -81,15 +83,17 @@ "AUTH_LOGIN_2SA": DSM_6_AUTH_LOGIN_2SA, "AUTH_LOGIN_2SA_OTP": DSM_6_AUTH_LOGIN_2SA_OTP, "DSM_INFORMATION": DSM_6_DSM_INFORMATION, - "DSM_NETWORK": DSM_6_DSM_NETWORK, + "DSM_NETWORK": DSM_6_DSM_NETWORK_2LAN_1PPPOE, "CORE_SECURITY": DSM_6_CORE_SECURITY, - "CORE_UTILIZATION": DSM_6_CORE_UTILIZATION, "CORE_SHARE": DSM_6_CORE_SHARE, + "CORE_SYSTEM": DSM_6_CORE_SYSTEM_DS918_PLUS, + "CORE_UTILIZATION": DSM_6_CORE_UTILIZATION, + "CORE_UPGRADE": DSM_6_CORE_UPGRADE_TRUE, "STORAGE_STORAGE": { "RAID": DSM_6_STORAGE_STORAGE_DS918_PLUS_RAID5_3DISKS_1VOL, "SHR1": DSM_6_STORAGE_STORAGE_DS213_PLUS_SHR1_2DISKS_2VOLS, "SHR2": DSM_6_STORAGE_STORAGE_DS1819_PLUS_SHR2_8DISKS_1VOL, - "SHR2_EXPANSION": DSM_6_STORAGE_STORAGE_DS1515_PLUS_SHR2_10DISKS_1VOL_WITH_EXPANSION, + "SHR2_EXPANSION": DSM_6_STORAGE_STORAGE_DS1515_PLUS_SHR2_10DISKS_1VOL_WITH_EXPANSION, # noqa: B950 }, }, } @@ -97,7 +101,8 @@ VALID_HOST = "nas.mywebsite.me" VALID_PORT = "443" -VALID_SSL = True +VALID_HTTPS = True +VALID_VERIFY_SSL = True VALID_USER = "valid_user" VALID_USER_2SA = "valid_user_2sa" VALID_PASSWORD = "valid_password" @@ -118,10 +123,12 @@ def __init__( username, password, use_https=False, + verify_ssl=False, timeout=None, device_token=None, debugmode=False, ): + """Constructor method.""" SynologyDSM.__init__( self, dsm_ip, @@ -129,11 +136,13 @@ def __init__( username, password, use_https, + verify_ssl, timeout, device_token, debugmode, ) + self.verify_ssl = verify_ssl self.dsm_version = 6 # 5 or 6 self.disks_redundancy = "RAID" # RAID or SHR[number][_EXPANSION] self.error = False @@ -145,14 +154,18 @@ def _execute_request(self, method, url, params, **kwargs): if "no_internet" in url: raise SynologyDSMRequestException( ConnError( - ": Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known" + ": Failed to establish a new connection: " + "[Errno 8] nodename nor servname provided, or not known" ) ) if VALID_HOST not in url: raise SynologyDSMRequestException( ConnError( - ": Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known" + ":" + " Failed to establish a new connection: [Errno 8] nodename " + "nor servname provided, or not known" ) ) @@ -171,6 +184,11 @@ def _execute_request(self, method, url, params, **kwargs): if "https" not in url: raise SynologyDSMRequestException(RequestException("Bad request")) + if not self.verify_ssl: + raise SynologyDSMRequestException( + SSLError(f"hostname '192.168.0.35' doesn't match '{VALID_HOST}'") + ) + if API_INFO in url: if self.with_surveillance: return DSM_6_API_INFO_SURVEILLANCE_STATION @@ -209,10 +227,15 @@ def _execute_request(self, method, url, params, **kwargs): if SynoCoreShare.API_KEY in url: return API_SWITCHER[self.dsm_version]["CORE_SHARE"] - if SynoCoreUtilization.API_KEY in url: - if self.error: - return DSM_6_CORE_UTILIZATION_ERROR_1055 - return API_SWITCHER[self.dsm_version]["CORE_UTILIZATION"] + if SynoCoreSystem.API_KEY in url: + if SynoCoreUtilization.API_KEY in url: + if self.error: + return DSM_6_CORE_UTILIZATION_ERROR_1055 + return API_SWITCHER[self.dsm_version]["CORE_UTILIZATION"] + return API_SWITCHER[self.dsm_version]["CORE_SYSTEM"] + + if SynoCoreUpgrade.API_KEY in url: + return API_SWITCHER[self.dsm_version]["CORE_UPGRADE"] if SynoDSMInformation.API_KEY in url: return API_SWITCHER[self.dsm_version]["DSM_INFORMATION"] diff --git a/tests/api_data/dsm_5/__init__.py b/tests/api_data/dsm_5/__init__.py index 56200968..ff260e95 100644 --- a/tests/api_data/dsm_5/__init__.py +++ b/tests/api_data/dsm_5/__init__.py @@ -1,13 +1,22 @@ """DSM 5 datas.""" +from .const_5_api_auth import DSM_5_AUTH_LOGIN +from .const_5_api_auth import DSM_5_AUTH_LOGIN_2SA +from .const_5_api_auth import DSM_5_AUTH_LOGIN_2SA_OTP from .const_5_api_info import DSM_5_API_INFO -from .const_5_api_auth import ( - DSM_5_AUTH_LOGIN, - DSM_5_AUTH_LOGIN_2SA, - DSM_5_AUTH_LOGIN_2SA_OTP, -) from .core.const_5_core_utilization import DSM_5_CORE_UTILIZATION from .dsm.const_5_dsm_info import DSM_5_DSM_INFORMATION from .dsm.const_5_dsm_network import DSM_5_DSM_NETWORK from .storage.const_5_storage_storage import ( DSM_5_STORAGE_STORAGE_DS410J_RAID5_4DISKS_1VOL, ) + +__all__ = [ + "DSM_5_AUTH_LOGIN", + "DSM_5_AUTH_LOGIN_2SA", + "DSM_5_AUTH_LOGIN_2SA_OTP", + "DSM_5_API_INFO", + "DSM_5_CORE_UTILIZATION", + "DSM_5_DSM_INFORMATION", + "DSM_5_DSM_NETWORK", + "DSM_5_STORAGE_STORAGE_DS410J_RAID5_4DISKS_1VOL", +] diff --git a/tests/api_data/dsm_5/const_5_api_auth.py b/tests/api_data/dsm_5/const_5_api_auth.py index 7f197e8e..0db2b521 100644 --- a/tests/api_data/dsm_5/const_5_api_auth.py +++ b/tests/api_data/dsm_5/const_5_api_auth.py @@ -1,9 +1,7 @@ """DSM 5 SYNO.API.Auth data.""" -from tests.const import ( - SESSION_ID, - DEVICE_TOKEN, - ERROR_AUTH_OTP_NOT_SPECIFIED, -) +from tests.const import DEVICE_TOKEN +from tests.const import ERROR_AUTH_OTP_NOT_SPECIFIED +from tests.const import SESSION_ID # No synotoken for an unknown reason DSM_5_AUTH_LOGIN = { diff --git a/tests/api_data/dsm_6/__init__.py b/tests/api_data/dsm_6/__init__.py index b74f9e37..394e7587 100644 --- a/tests/api_data/dsm_6/__init__.py +++ b/tests/api_data/dsm_6/__init__.py @@ -1,21 +1,22 @@ """DSM 6 datas.""" +from .const_6_api_auth import DSM_6_AUTH_LOGIN +from .const_6_api_auth import DSM_6_AUTH_LOGIN_2SA +from .const_6_api_auth import DSM_6_AUTH_LOGIN_2SA_OTP from .const_6_api_info import DSM_6_API_INFO -from .const_6_api_auth import ( - DSM_6_AUTH_LOGIN, - DSM_6_AUTH_LOGIN_2SA, - DSM_6_AUTH_LOGIN_2SA_OTP, -) -from .core.const_6_core_utilization import ( - DSM_6_CORE_UTILIZATION, - DSM_6_CORE_UTILIZATION_ERROR_1055, -) -from .core.const_6_core_security import ( - DSM_6_CORE_SECURITY, - DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE, +from .core.const_6_core_security import DSM_6_CORE_SECURITY +from .core.const_6_core_security import DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE +from .core.const_6_core_share import DSM_6_CORE_SHARE +from .core.const_6_core_system import DSM_6_CORE_SYSTEM_DS218_PLAY +from .core.const_6_core_system import DSM_6_CORE_SYSTEM_DS918_PLUS +from .core.const_6_core_upgrade import DSM_6_CORE_UPGRADE_FALSE +from .core.const_6_core_upgrade import DSM_6_CORE_UPGRADE_TRUE +from .core.const_6_core_utilization import DSM_6_CORE_UTILIZATION +from .core.const_6_core_utilization import DSM_6_CORE_UTILIZATION_ERROR_1055 +from .download_station.const_6_download_station_info import ( + DSM_6_DOWNLOAD_STATION_INFO_CONFIG, ) from .download_station.const_6_download_station_info import ( DSM_6_DOWNLOAD_STATION_INFO_INFO, - DSM_6_DOWNLOAD_STATION_INFO_CONFIG, ) from .download_station.const_6_download_station_stat import ( DSM_6_DOWNLOAD_STATION_STAT_INFO, @@ -24,24 +25,70 @@ DSM_6_DOWNLOAD_STATION_TASK_LIST, ) from .dsm.const_6_dsm_info import DSM_6_DSM_INFORMATION -from .dsm.const_6_dsm_network import DSM_6_DSM_NETWORK +from .dsm.const_6_dsm_network import DSM_6_DSM_NETWORK_2LAN_1PPPOE from .storage.const_6_storage_storage import ( - DSM_6_STORAGE_STORAGE_DS213_PLUS_SHR1_2DISKS_2VOLS, - DSM_6_STORAGE_STORAGE_DS918_PLUS_RAID5_3DISKS_1VOL, DSM_6_STORAGE_STORAGE_DS1515_PLUS_SHR2_10DISKS_1VOL_WITH_EXPANSION, +) +from .storage.const_6_storage_storage import ( DSM_6_STORAGE_STORAGE_DS1819_PLUS_SHR2_8DISKS_1VOL, ) +from .storage.const_6_storage_storage import ( + DSM_6_STORAGE_STORAGE_DS213_PLUS_SHR1_2DISKS_2VOLS, +) +from .storage.const_6_storage_storage import ( + DSM_6_STORAGE_STORAGE_DS918_PLUS_RAID5_3DISKS_1VOL, +) from .surveillance_station.const_6_api_info import ( DSM_6_API_INFO as DSM_6_API_INFO_SURVEILLANCE_STATION, ) from .surveillance_station.const_6_surveillance_station_camera import ( - DSM_6_SURVEILLANCE_STATION_CAMERA_LIST, - DSM_6_SURVEILLANCE_STATION_CAMERA_GET_LIVE_VIEW_PATH, - DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MOTION_ENUM, DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MD_PARAM_SAVE, ) -from .core.const_6_core_share import DSM_6_CORE_SHARE +from .surveillance_station.const_6_surveillance_station_camera import ( + DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MOTION_ENUM, +) +from .surveillance_station.const_6_surveillance_station_camera import ( + DSM_6_SURVEILLANCE_STATION_CAMERA_GET_LIVE_VIEW_PATH, +) +from .surveillance_station.const_6_surveillance_station_camera import ( + DSM_6_SURVEILLANCE_STATION_CAMERA_LIST, +) from .surveillance_station.const_6_surveillance_station_home_mode import ( DSM_6_SURVEILLANCE_STATION_HOME_MODE_GET_INFO, +) +from .surveillance_station.const_6_surveillance_station_home_mode import ( DSM_6_SURVEILLANCE_STATION_HOME_MODE_SWITCH, ) + +__all__ = [ + "DSM_6_AUTH_LOGIN", + "DSM_6_AUTH_LOGIN_2SA", + "DSM_6_AUTH_LOGIN_2SA_OTP", + "DSM_6_API_INFO", + "DSM_6_CORE_SECURITY", + "DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE", + "DSM_6_CORE_SHARE", + "DSM_6_CORE_SYSTEM_DS218_PLAY", + "DSM_6_CORE_SYSTEM_DS918_PLUS", + "DSM_6_CORE_UPGRADE_FALSE", + "DSM_6_CORE_UPGRADE_TRUE", + "DSM_6_CORE_UTILIZATION", + "DSM_6_CORE_UTILIZATION_ERROR_1055", + "DSM_6_DOWNLOAD_STATION_INFO_CONFIG", + "DSM_6_DOWNLOAD_STATION_INFO_INFO", + "DSM_6_DOWNLOAD_STATION_STAT_INFO", + "DSM_6_DOWNLOAD_STATION_TASK_LIST", + "DSM_6_DSM_INFORMATION", + "DSM_6_DSM_NETWORK_2LAN_1PPPOE", + "DSM_6_STORAGE_STORAGE_DS1515_PLUS_SHR2_10DISKS_1VOL_WITH_EXPANSION", + "DSM_6_STORAGE_STORAGE_DS1819_PLUS_SHR2_8DISKS_1VOL", + "DSM_6_STORAGE_STORAGE_DS213_PLUS_SHR1_2DISKS_2VOLS", + "DSM_6_STORAGE_STORAGE_DS918_PLUS_RAID5_3DISKS_1VOL", + "DSM_6_API_INFO_SURVEILLANCE_STATION", + "DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MD_PARAM_SAVE", + "DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MOTION_ENUM", + "DSM_6_SURVEILLANCE_STATION_CAMERA_GET_LIVE_VIEW_PATH", + "DSM_6_SURVEILLANCE_STATION_CAMERA_LIST", + "DSM_6_SURVEILLANCE_STATION_HOME_MODE_GET_INFO", + "DSM_6_SURVEILLANCE_STATION_HOME_MODE_SWITCH", +] diff --git a/tests/api_data/dsm_6/const_6_api_auth.py b/tests/api_data/dsm_6/const_6_api_auth.py index 13c6ff06..771ed332 100644 --- a/tests/api_data/dsm_6/const_6_api_auth.py +++ b/tests/api_data/dsm_6/const_6_api_auth.py @@ -1,10 +1,8 @@ """DSM 6 SYNO.API.Auth data.""" -from tests.const import ( - SESSION_ID, - DEVICE_TOKEN, - SYNO_TOKEN, - ERROR_AUTH_OTP_NOT_SPECIFIED, -) +from tests.const import DEVICE_TOKEN +from tests.const import ERROR_AUTH_OTP_NOT_SPECIFIED +from tests.const import SESSION_ID +from tests.const import SYNO_TOKEN DSM_6_AUTH_LOGIN = { @@ -21,3 +19,5 @@ }, "success": True, } + +DSM_6_AUTH_LOGOUT = {"success": True} diff --git a/tests/api_data/dsm_6/core/const_6_core_system.py b/tests/api_data/dsm_6/core/const_6_core_system.py index 2073e436..74247090 100644 --- a/tests/api_data/dsm_6/core/const_6_core_system.py +++ b/tests/api_data/dsm_6/core/const_6_core_system.py @@ -1,38 +1,108 @@ """DSM 6 SYNO.Core.System data.""" -DSM_6_CORE_SYSTEM = { - "data":{ - "cpu_clock_speed":1400, - "cpu_cores":"4", - "cpu_family":"RTD1296", - "cpu_series":"SoC", - "cpu_vendor":"Realtek", - "enabled_ntp":True, - "firmware_date":"2020/07/14", - "firmware_ver":"DSM 6.2.3-25426 Update 2", - "model":"DS218play", - "ntp_server":"pool.ntp.org", - "ram_size":1024, - "serial":"123456abcdefg", - "support_esata":"no", - "sys_temp":40, - "sys_tempwarn":False, - "systempwarn":False, - "temperature_warning":False, - "time":"2020-10-16 20:26:58", - "time_zone":"Amsterdam", - "time_zone_desc":"(GMT+01:00) Amsterdam, Berlin, Rome, Stockholm, Vienna", - "up_time":"289:31:54", - "usb_dev":[ - { - "cls":"disk", - "pid":"2621", - "producer":"Western Digital Technologies, Inc.", - "product":"Elements 2621", - "rev":"10.26", - "vid":"1058" - } - ] - }, - "success":True +DSM_6_CORE_SYSTEM_DS918_PLUS = { + "data": { + "cpu_clock_speed": 1500, + "cpu_cores": "4", + "cpu_family": "Celeron", + "cpu_series": "J3455", + "cpu_vendor": "INTEL", + "enabled_ntp": True, + "firmware_date": "2020/07/08", + "firmware_ver": "DSM 6.2.3-25426 Update 2", + "model": "DS918+", + "ntp_server": "time.google.com", + "ram_size": 4096, + "sata_dev": [], + "serial": "1920PDN001501", + "support_esata": "yes", + "sys_temp": 40, + "sys_tempwarn": False, + "systempwarn": False, + "temperature_warning": False, + "time": "2020-10-19 23:33:52", + "time_zone": "Brussels", + "time_zone_desc": "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris", + "up_time": "75:12:9", + "usb_dev": [ + { + "cls": "hub", + "pid": "0612", + "producer": "Genesys Logic, Inc.", + "product": "Hub", + "rev": "92.23", + "vid": "05e3", + }, + { + "cls": "other", + "pid": "1790", + "producer": "ASIX Electronics Corp.", + "product": "AX88179 Gigabit Ethernet", + "rev": "1.00", + "vid": "0b95", + }, + { + "cls": "hub", + "pid": "0610", + "producer": "Genesys Logic, Inc.", + "product": "4-port hub", + "rev": "92.23", + "vid": "05e3", + }, + { + "cls": "other", + "pid": "0200", + "producer": "Sigma Designs, Inc.", + "product": "Aeotec Z-Stick Gen5 (ZW090) - UZB", + "rev": "0.00", + "vid": "0658", + }, + { + "cls": "ups", + "pid": "0002", + "producer": "American Power Conversion", + "product": "Uninterruptible Power Supply", + "rev": "1.06", + "vid": "051d", + }, + ], + }, + "success": True, +} + +DSM_6_CORE_SYSTEM_DS218_PLAY = { + "data": { + "cpu_clock_speed": 1400, + "cpu_cores": "4", + "cpu_family": "RTD1296", + "cpu_series": "SoC", + "cpu_vendor": "Realtek", + "enabled_ntp": True, + "firmware_date": "2020/07/14", + "firmware_ver": "DSM 6.2.3-25426 Update 2", + "model": "DS218play", + "ntp_server": "pool.ntp.org", + "ram_size": 1024, + "serial": "123456abcdefg", + "support_esata": "no", + "sys_temp": 40, + "sys_tempwarn": False, + "systempwarn": False, + "temperature_warning": False, + "time": "2020-10-16 20:26:58", + "time_zone": "Amsterdam", + "time_zone_desc": "(GMT+01:00) Amsterdam, Berlin, Rome, Stockholm, Vienna", + "up_time": "289:31:54", + "usb_dev": [ + { + "cls": "disk", + "pid": "2621", + "producer": "Western Digital Technologies, Inc.", + "product": "Elements 2621", + "rev": "10.26", + "vid": "1058", + } + ], + }, + "success": True, } diff --git a/tests/api_data/dsm_6/core/const_6_core_upgrade.py b/tests/api_data/dsm_6/core/const_6_core_upgrade.py index 8b393ed0..a1c4ed71 100644 --- a/tests/api_data/dsm_6/core/const_6_core_upgrade.py +++ b/tests/api_data/dsm_6/core/const_6_core_upgrade.py @@ -1,10 +1,23 @@ """DSM 6 SYNO.Core.Upgrade data.""" -DSM_6_CORE_SYSTEM = { - "data":{ - "update":{ - "available":False - } - }, - "success":True +DSM_6_CORE_UPGRADE_FALSE = {"data": {"update": {"available": False}}, "success": True} +DSM_6_CORE_UPGRADE_TRUE = { + "data": { + "update": { + "available": True, + "reboot": "now", + "restart": "some", + "type": "nano", + "version": "DSM 6.2.3-25426 Update 2", + "version_details": { + "buildnumber": 25426, + "major": 6, + "micro": 3, + "minor": 2, + "nano": 2, + "os_name": "DSM", + }, + } + }, + "success": True, } diff --git a/tests/api_data/dsm_6/dsm/const_6_dsm_network.py b/tests/api_data/dsm_6/dsm/const_6_dsm_network.py index 38a6d05f..600151ac 100644 --- a/tests/api_data/dsm_6/dsm/const_6_dsm_network.py +++ b/tests/api_data/dsm_6/dsm/const_6_dsm_network.py @@ -1,6 +1,6 @@ """DSM 6 SYNO.DSM.Network data.""" -DSM_6_DSM_NETWORK = { +DSM_6_DSM_NETWORK_1LAN = { "data": { "dns": ["192.168.0.35"], "gateway": "192.168.0.254", @@ -35,3 +35,56 @@ }, "success": True, } + +DSM_6_DSM_NETWORK_2LAN_1PPPOE = { + "data": { + "dns": ["192.168.0.35"], + "gateway": "192.168.0.254", + "hostname": "NAS_[NAME]", + "interfaces": [ + { + "id": "eth0", + "ip": [{"address": "192.168.5.10", "netmask": "255.255.255.0"}], + "ipv6": [ + { + "address": "2001:b211:317c:147e:211:32ff:fe5d:fd11", + "prefix_length": 64, + "scope": "global", + }, + { + "address": "fe80::211:32ff:fe5d:fd11", + "prefix_length": 64, + "scope": "link", + }, + ], + "mac": "00-11-32-XX-XX-11", + "type": "lan", + }, + { + "id": "eth1", + "ip": [{"address": "192.168.1.100", "netmask": "255.255.255.0"}], + "ipv6": [ + { + "address": "2001:b011:300c:176c:211:11ff:fe5d:fd12", + "prefix_length": 64, + "scope": "global", + }, + { + "address": "fe80::211:31ff:ff5d:fd12", + "prefix_length": 64, + "scope": "link", + }, + ], + "mac": "00-11-32-XX-XX-12", + "type": "lan", + }, + { + "id": "ppp0", + "ip": [{"address": "114.45.2.158", "netmask": "255.255.255.255"}], + "type": "pppoe", + }, + ], + "workgroup": "WORKGROUP", + }, + "success": True, +} diff --git a/tests/const.py b/tests/const.py index 6e938f53..16f7e869 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,5 +1,4 @@ """Test constants.""" - # API test data are localized in # `tests/api_data/dsm_[dsm_major_version]` # Data constant names should be like : diff --git a/tests/test_synology_dsm.py b/tests/test_synology_dsm.py index 87071b24..d24023a9 100644 --- a/tests/test_synology_dsm.py +++ b/tests/test_synology_dsm.py @@ -1,34 +1,32 @@ """Synology DSM tests.""" from unittest import TestCase + import pytest +from . import SynologyDSMMock +from . import USER_MAX_TRY +from . import VALID_HOST +from . import VALID_HTTPS +from . import VALID_OTP +from . import VALID_PASSWORD +from . import VALID_PORT +from . import VALID_USER +from . import VALID_USER_2SA +from . import VALID_VERIFY_SSL +from .const import DEVICE_TOKEN +from .const import SESSION_ID +from .const import SYNO_TOKEN from synology_dsm.api.core.security import SynoCoreSecurity from synology_dsm.api.dsm.information import SynoDSMInformation -from synology_dsm.exceptions import ( - SynologyDSMRequestException, - SynologyDSMAPINotExistsException, - SynologyDSMAPIErrorException, - SynologyDSMLoginInvalidException, - SynologyDSMLogin2SARequiredException, - SynologyDSMLogin2SAFailedException, - SynologyDSMLoginFailedException, -) -from synology_dsm.const import API_AUTH, API_INFO - -from . import ( - SynologyDSMMock, - VALID_HOST, - VALID_PORT, - VALID_SSL, - VALID_OTP, - VALID_PASSWORD, - VALID_USER, - VALID_USER_2SA, - USER_MAX_TRY, -) -from .const import SESSION_ID, DEVICE_TOKEN, SYNO_TOKEN - -# pylint: disable=no-self-use,protected-access +from synology_dsm.const import API_AUTH +from synology_dsm.const import API_INFO +from synology_dsm.exceptions import SynologyDSMAPIErrorException +from synology_dsm.exceptions import SynologyDSMAPINotExistsException +from synology_dsm.exceptions import SynologyDSMLogin2SAFailedException +from synology_dsm.exceptions import SynologyDSMLogin2SARequiredException +from synology_dsm.exceptions import SynologyDSMLoginFailedException +from synology_dsm.exceptions import SynologyDSMLoginInvalidException +from synology_dsm.exceptions import SynologyDSMRequestException class TestSynologyDSM(TestCase): @@ -37,8 +35,14 @@ class TestSynologyDSM(TestCase): api = None def setUp(self): + """Context initialisation called for all tests.""" self.api = SynologyDSMMock( - VALID_HOST, VALID_PORT, VALID_USER, VALID_PASSWORD, VALID_SSL + VALID_HOST, + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) def test_init(self): @@ -53,7 +57,12 @@ def test_connection_failed(self): """Test failed connection.""" # No internet api = SynologyDSMMock( - "no_internet", VALID_PORT, VALID_USER, VALID_PASSWORD, VALID_SSL + "no_internet", + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) with pytest.raises(SynologyDSMRequestException) as error: api.login() @@ -70,7 +79,14 @@ def test_connection_failed(self): assert not api._session_id # Wrong host - api = SynologyDSMMock("host", VALID_PORT, VALID_USER, VALID_PASSWORD, VALID_SSL) + api = SynologyDSMMock( + "host", + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, + ) with pytest.raises(SynologyDSMRequestException) as error: api.login() error_value = error.value.args[0] @@ -86,23 +102,32 @@ def test_connection_failed(self): assert not api._session_id # Wrong port - api = SynologyDSMMock(VALID_HOST, 0, VALID_USER, VALID_PASSWORD, VALID_SSL) + api = SynologyDSMMock( + VALID_HOST, 0, VALID_USER, VALID_PASSWORD, VALID_HTTPS, VALID_VERIFY_SSL + ) with pytest.raises(SynologyDSMRequestException) as error: api.login() error_value = error.value.args[0] assert not error_value["api"] assert error_value["code"] == -1 assert error_value["reason"] == "Unknown" - assert ( - error_value["details"] - == "SSLError = [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1076)" + assert error_value["details"] == ( + "SSLError = [SSL: WRONG_VERSION_NUMBER] " + "wrong version number (_ssl.c:1076)" ) assert not api.apis.get(API_AUTH) assert not api._session_id - # Wrong SSL - api = SynologyDSMMock(VALID_HOST, VALID_PORT, VALID_USER, VALID_PASSWORD, False) + # Wrong HTTPS + api = SynologyDSMMock( + VALID_HOST, + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + False, + VALID_VERIFY_SSL, + ) with pytest.raises(SynologyDSMRequestException) as error: api.login() error_value = error.value.args[0] @@ -114,6 +139,29 @@ def test_connection_failed(self): assert not api.apis.get(API_AUTH) assert not api._session_id + # Wrong SSL + api = SynologyDSMMock( + VALID_HOST, + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + VALID_HTTPS, + False, + ) + with pytest.raises(SynologyDSMRequestException) as error: + api.login() + error_value = error.value.args[0] + assert not error_value["api"] + assert error_value["code"] == -1 + assert error_value["reason"] == "Unknown" + assert ( + error_value["details"] + == f"SSLError = hostname '192.168.0.35' doesn't match '{VALID_HOST}'" + ) + + assert not api.apis.get(API_AUTH) + assert not api._session_id + def test_login(self): """Test login.""" assert self.api.login() @@ -123,7 +171,14 @@ def test_login(self): def test_login_failed(self): """Test failed login.""" - api = SynologyDSMMock(VALID_HOST, VALID_PORT, "user", VALID_PASSWORD, VALID_SSL) + api = SynologyDSMMock( + VALID_HOST, + VALID_PORT, + "user", + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, + ) with pytest.raises(SynologyDSMLoginInvalidException) as error: api.login() error_value = error.value.args[0] @@ -135,7 +190,14 @@ def test_login_failed(self): assert api.apis.get(API_AUTH) assert not api._session_id - api = SynologyDSMMock(VALID_HOST, VALID_PORT, VALID_USER, "pass", VALID_SSL) + api = SynologyDSMMock( + VALID_HOST, + VALID_PORT, + VALID_USER, + "pass", + VALID_HTTPS, + VALID_VERIFY_SSL, + ) with pytest.raises(SynologyDSMLoginInvalidException) as error: api.login() error_value = error.value.args[0] @@ -153,7 +215,12 @@ def test_login_failed(self): def test_login_2sa(self): """Test login with 2SA.""" api = SynologyDSMMock( - VALID_HOST, VALID_PORT, VALID_USER_2SA, VALID_PASSWORD, VALID_SSL + VALID_HOST, + VALID_PORT, + VALID_USER_2SA, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) with pytest.raises(SynologyDSMLogin2SARequiredException) as error: @@ -181,7 +248,8 @@ def test_login_2sa_new_session(self): VALID_PORT, VALID_USER_2SA, VALID_PASSWORD, - VALID_SSL, + VALID_HTTPS, + VALID_VERIFY_SSL, device_token=DEVICE_TOKEN, ) assert api.login() @@ -194,7 +262,12 @@ def test_login_2sa_new_session(self): def test_login_2sa_failed(self): """Test failed login with 2SA.""" api = SynologyDSMMock( - VALID_HOST, VALID_PORT, VALID_USER_2SA, VALID_PASSWORD, VALID_SSL + VALID_HOST, + VALID_PORT, + VALID_USER_2SA, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) with pytest.raises(SynologyDSMLogin2SARequiredException) as error: @@ -226,7 +299,12 @@ def test_login_2sa_failed(self): def test_login_basic_failed(self): """Test basic failed login.""" api = SynologyDSMMock( - VALID_HOST, VALID_PORT, USER_MAX_TRY, VALID_PASSWORD, VALID_SSL + VALID_HOST, + VALID_PORT, + USER_MAX_TRY, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) with pytest.raises(SynologyDSMLoginFailedException) as error: @@ -240,7 +318,13 @@ def test_login_basic_failed(self): def test_request_timeout(self): """Test request timeout.""" api = SynologyDSMMock( - VALID_HOST, VALID_PORT, VALID_USER, VALID_PASSWORD, VALID_SSL, timeout=2 + VALID_HOST, + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, + timeout=2, ) assert api._timeout == 2 @@ -295,9 +379,9 @@ def test_request_post_failed(self): error_value = error.value.args[0] assert error_value["api"] == "SYNO.FileStation.Upload" assert error_value["code"] == 1805 - assert ( - error_value["reason"] - == "Can’t overwrite or skip the existed file, if no overwrite parameter is given" + assert error_value["reason"] == ( + "Can’t overwrite or skip the existed file, if no overwrite" + " parameter is given" ) assert not error_value["details"] @@ -428,6 +512,73 @@ def test_security_error(self): assert self.api.security.status_by_check["update"] == "outOfDate" assert self.api.security.status_by_check["userInfo"] == "safe" + def test_shares(self): + """Test shares.""" + assert self.api.share + self.api.share.update() + assert self.api.share.shares + for share_uuid in self.api.share.shares_uuids: + assert self.api.share.share_name(share_uuid) + assert self.api.share.share_path(share_uuid) + assert self.api.share.share_recycle_bin(share_uuid) is not None + assert self.api.share.share_size(share_uuid) is not None + assert self.api.share.share_size(share_uuid, human_readable=True) + + assert ( + self.api.share.share_name("2ee6c06a-8766-48b5-013d-63b18652a393") + == "test_share" + ) + assert ( + self.api.share.share_path("2ee6c06a-8766-48b5-013d-63b18652a393") + == "/volume1" + ) + assert ( + self.api.share.share_recycle_bin("2ee6c06a-8766-48b5-013d-63b18652a393") + is True + ) + assert ( + self.api.share.share_size("2ee6c06a-8766-48b5-013d-63b18652a393") + == 3.790251876432216e19 + ) + assert ( + self.api.share.share_size("2ee6c06a-8766-48b5-013d-63b18652a393", True) + == "32.9Eb" + ) + + def test_system(self): + """Test system.""" + assert self.api.system + self.api.system.update() + assert self.api.system.cpu_clock_speed + assert self.api.system.cpu_cores + assert self.api.system.cpu_family + assert self.api.system.cpu_series + assert self.api.system.firmware_ver + assert self.api.system.model + assert self.api.system.ram_size + assert self.api.system.serial + assert self.api.system.sys_temp + assert self.api.system.time + assert self.api.system.time_zone + assert self.api.system.time_zone_desc + assert self.api.system.up_time + for usb_dev in self.api.system.usb_dev: + assert usb_dev.get("cls") + assert usb_dev.get("pid") + assert usb_dev.get("producer") + assert usb_dev.get("product") + assert usb_dev.get("rev") + assert usb_dev.get("vid") + + def test_upgrade(self): + """Test upgrade.""" + assert self.api.upgrade + self.api.upgrade.update() + assert self.api.upgrade.update_available + assert self.api.upgrade.available_version == "DSM 6.2.3-25426 Update 2" + assert self.api.upgrade.reboot_needed == "now" + assert self.api.upgrade.service_restarts == "some" + def test_utilisation(self): """Test utilisation.""" assert self.api.utilisation @@ -763,36 +914,3 @@ def test_surveillance_station(self): assert self.api.surveillance_station.get_home_mode_status() assert self.api.surveillance_station.set_home_mode(False) assert self.api.surveillance_station.set_home_mode(True) - - def test_shares(self): - """Test shares.""" - assert self.api.share - self.api.share.update() - assert self.api.share.shares - for share_uuid in self.api.share.shares_uuids: - assert self.api.share.share_name(share_uuid) - assert self.api.share.share_path(share_uuid) - assert self.api.share.share_recycle_bin(share_uuid) is not None - assert self.api.share.share_size(share_uuid) is not None - assert self.api.share.share_size(share_uuid, human_readable=True) - - assert ( - self.api.share.share_name("2ee6c06a-8766-48b5-013d-63b18652a393") - == "test_share" - ) - assert ( - self.api.share.share_path("2ee6c06a-8766-48b5-013d-63b18652a393") - == "/volume1" - ) - assert ( - self.api.share.share_recycle_bin("2ee6c06a-8766-48b5-013d-63b18652a393") - is True - ) - assert ( - self.api.share.share_size("2ee6c06a-8766-48b5-013d-63b18652a393") - == 3.790251876432216e19 - ) - assert ( - self.api.share.share_size("2ee6c06a-8766-48b5-013d-63b18652a393", True) - == "32.9Eb" - ) diff --git a/tests/test_synology_dsm_5.py b/tests/test_synology_dsm_5.py index ee0577c7..488db00e 100644 --- a/tests/test_synology_dsm_5.py +++ b/tests/test_synology_dsm_5.py @@ -1,37 +1,41 @@ """Synology DSM tests.""" from unittest import TestCase -from synology_dsm.exceptions import ( - SynologyDSMRequestException, - SynologyDSMAPINotExistsException, - SynologyDSMAPIErrorException, - SynologyDSMLoginInvalidException, - SynologyDSMLogin2SARequiredException, - SynologyDSMLogin2SAFailedException, -) -from synology_dsm.const import API_AUTH, API_INFO - -from . import ( - SynologyDSMMock, - VALID_HOST, - VALID_PORT, - VALID_SSL, - VALID_OTP, - VALID_PASSWORD, - VALID_USER, - VALID_USER_2SA, -) -from .const import SESSION_ID, DEVICE_TOKEN - -# pylint: disable=no-self-use,protected-access,anomalous-backslash-in-string +from . import SynologyDSMMock +from . import VALID_HOST +from . import VALID_HTTPS +from . import VALID_OTP +from . import VALID_PASSWORD +from . import VALID_PORT +from . import VALID_USER +from . import VALID_USER_2SA +from . import VALID_VERIFY_SSL +from .const import DEVICE_TOKEN +from .const import SESSION_ID +from synology_dsm.const import API_AUTH +from synology_dsm.const import API_INFO +from synology_dsm.exceptions import SynologyDSMAPIErrorException +from synology_dsm.exceptions import SynologyDSMAPINotExistsException +from synology_dsm.exceptions import SynologyDSMLogin2SAFailedException +from synology_dsm.exceptions import SynologyDSMLogin2SARequiredException +from synology_dsm.exceptions import SynologyDSMLoginInvalidException +from synology_dsm.exceptions import SynologyDSMRequestException + + class TestSynologyDSM(TestCase): """SynologyDSM test cases.""" api = None def setUp(self): + """Context initialisation called for all tests.""" self.api = SynologyDSMMock( - VALID_HOST, VALID_PORT, VALID_USER, VALID_PASSWORD, VALID_SSL + VALID_HOST, + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) self.api.dsm_version = 5 @@ -45,7 +49,12 @@ def test_init(self): def test_connection_failed(self): """Test failed connection.""" api = SynologyDSMMock( - "no_internet", VALID_PORT, VALID_USER, VALID_PASSWORD, VALID_SSL + "no_internet", + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) api.dsm_version = 5 with self.assertRaises(SynologyDSMRequestException): @@ -53,21 +62,37 @@ def test_connection_failed(self): assert not api.apis.get(API_AUTH) assert not api._session_id - api = SynologyDSMMock("host", VALID_PORT, VALID_USER, VALID_PASSWORD, VALID_SSL) + api = SynologyDSMMock( + "host", + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, + ) api.dsm_version = 5 with self.assertRaises(SynologyDSMRequestException): assert not api.login() assert not api.apis.get(API_AUTH) assert not api._session_id - api = SynologyDSMMock(VALID_HOST, 0, VALID_USER, VALID_PASSWORD, VALID_SSL) + api = SynologyDSMMock( + VALID_HOST, 0, VALID_USER, VALID_PASSWORD, VALID_HTTPS, VALID_VERIFY_SSL + ) api.dsm_version = 5 with self.assertRaises(SynologyDSMRequestException): assert not api.login() assert not api.apis.get(API_AUTH) assert not api._session_id - api = SynologyDSMMock(VALID_HOST, VALID_PORT, VALID_USER, VALID_PASSWORD, False) + api = SynologyDSMMock( + VALID_HOST, + VALID_PORT, + VALID_USER, + VALID_PASSWORD, + False, + VALID_VERIFY_SSL, + ) api.dsm_version = 5 with self.assertRaises(SynologyDSMRequestException): assert not api.login() @@ -83,14 +108,28 @@ def test_login(self): def test_login_failed(self): """Test failed login.""" - api = SynologyDSMMock(VALID_HOST, VALID_PORT, "user", VALID_PASSWORD, VALID_SSL) + api = SynologyDSMMock( + VALID_HOST, + VALID_PORT, + "user", + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, + ) api.dsm_version = 5 with self.assertRaises(SynologyDSMLoginInvalidException): assert not api.login() assert api.apis.get(API_AUTH) assert not api._session_id - api = SynologyDSMMock(VALID_HOST, VALID_PORT, VALID_USER, "pass", VALID_SSL) + api = SynologyDSMMock( + VALID_HOST, + VALID_PORT, + VALID_USER, + "pass", + VALID_HTTPS, + VALID_VERIFY_SSL, + ) api.dsm_version = 5 with self.assertRaises(SynologyDSMLoginInvalidException): assert not api.login() @@ -100,7 +139,12 @@ def test_login_failed(self): def test_login_2sa(self): """Test login with 2SA.""" api = SynologyDSMMock( - VALID_HOST, VALID_PORT, VALID_USER_2SA, VALID_PASSWORD, VALID_SSL + VALID_HOST, + VALID_PORT, + VALID_USER_2SA, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) api.dsm_version = 5 with self.assertRaises(SynologyDSMLogin2SARequiredException): @@ -119,7 +163,8 @@ def test_login_2sa_new_session(self): VALID_PORT, VALID_USER_2SA, VALID_PASSWORD, - VALID_SSL, + VALID_HTTPS, + VALID_VERIFY_SSL, device_token=DEVICE_TOKEN, ) api.dsm_version = 5 @@ -133,7 +178,12 @@ def test_login_2sa_new_session(self): def test_login_2sa_failed(self): """Test failed login with 2SA.""" api = SynologyDSMMock( - VALID_HOST, VALID_PORT, VALID_USER_2SA, VALID_PASSWORD, VALID_SSL + VALID_HOST, + VALID_PORT, + VALID_USER_2SA, + VALID_PASSWORD, + VALID_HTTPS, + VALID_VERIFY_SSL, ) api.dsm_version = 5 with self.assertRaises(SynologyDSMLogin2SARequiredException):