Migrate to poetry, nox and GitHub actions (#78)

* Replace setup files with pyproject.toml

Now we can use poetry to manage the package.
poetry install to install the package in a venv
poetry run ... to launch pylint, pytest or black

* Add CI with GitHub Action

Add a tests and release workflow
Add release drafter
Add dependabot

* Remove old CI based on travis and pylint

* Add linting configuration files

Linting mainly based on flake8 and plugins.
Add specific files for darglint and mypy.

* Update .gitignore

* Add test suite with nox and pre-commit

Test suite inculde pre-commit, safety, mypy, tests, typguard and documentation
checks.

* Add .gitattributes

* Update code coverage target to 80%

* Code style update by Black

* Update poetry lock file

* Deactivate temporarly some linting tests

* Add a contributing guide

* Remove Python 3.6 in GitHub Action

* Patch noxfile.py to be able to test mypy session

* Rebase version to 1.0.0
This commit is contained in:
Oncleben31
2020-10-21 21:38:25 +02:00
committed by GitHub
parent a4edbd3348
commit 9622f5d314
38 changed files with 2436 additions and 324 deletions

2
.darglint Normal file
View File

@ -0,0 +1,2 @@
[darglint]
strictness = short

12
.flake8 Normal file
View File

@ -0,0 +1,12 @@
[flake8]
select = B,B9,C,DAR,E,F,N,RST,S,W
# Some tests have been added to the ignore list to avoid reworking too much
# the code in this PR: D107, D403, B950, E266, C901, F401, E302.
# The error will be processed in a dedicated PR.
# targeted ignore list is:
# ignore = E203,E501,RST201,RST203,RST301,W503
ignore = E203,E501,RST201,RST203,RST301,W503, D107, D403, B950, E266, C901, F401, E302
max-line-length = 80
max-complexity = 10
docstring-convention = google
per-file-ignores = tests/*:S101

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

18
.github/dependabot.yml vendored Normal file
View File

@ -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

29
.github/release-drafter.yml vendored Normal file
View File

@ -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

5
.github/workflows/constraints.txt vendored Normal file
View File

@ -0,0 +1,5 @@
pip==20.2.4
nox==2020.8.22
nox-poetry==0.5.0
poetry==1.0.10
virtualenv==20.0.35

79
.github/workflows/release.yml vendored Normal file
View File

@ -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.3
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.0
with:
publish: ${{ steps.check-version.outputs.tag != '' }}
tag: ${{ steps.check-version.outputs.tag }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

140
.github/workflows/tests.yml vendored Normal file
View File

@ -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.3
- 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.2
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.0"
with:
name: coverage-data
path: ".coverage.*"
- name: Upload documentation
if: matrix.session == 'docs-build'
uses: actions/upload-artifact@v2.2.0
with:
name: docs
path: docs/_build
coverage:
runs-on: ubuntu-latest
needs: tests
steps:
- name: Check out the repository
uses: actions/checkout@v2.3.2
- 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.5
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.14

14
.gitignore vendored
View File

@ -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]

51
.pre-commit-config.yaml Normal file
View File

@ -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

View File

@ -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 src 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

121
CONTRIBUTING.rst Normal file
View File

@ -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/ProtoThis/python-synology
.. _Documentation: https://python-synology.readthedocs.io/
.. _Issue Tracker: https://github.com/ProtoThis/python-synology/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/ProtoThis/python-synology/pulls
.. github-only
.. _Code of Conduct: CODE_OF_CONDUCT.rst

9
codecov.yml Normal file
View File

@ -0,0 +1,9 @@
comment: false
coverage:
status:
project:
default:
target: "80"
patch:
default:
target: "100"

20
mypy.ini Normal file
View File

@ -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

188
noxfile.py Normal file
View File

@ -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)

1494
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -1,21 +1,76 @@
[tool.black]
line-length = 88
target-version = ["py27", "py33", "py34", "py35", "py36", "py37", "py38"]
exclude = '''
[tool.poetry]
name = "python-synology"
version = "1.0.0"
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/ProtoThis/python-synology"
repository = "https://github.com/ProtoThis/python-synology"
documentation = "https://python-synology.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" },
]
(
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
| exceptions.py
)
'''
[tool.poetry.urls]
Changelog = "https://github.com/ProtoThis/python-synology/releases"
[tool.poetry.dependencies]
python = "^3.7.0"
requests = "^2.24.0"
urllib3 = "^1.25.10"
[tool.poetry.dev-dependencies]
pytest = "^6.1.1"
coverage = {extras = ["toml"], version = "^5.3"}
safety = "^1.9.0"
mypy = "^0.782"
typeguard = "^2.9.1"
xdoctest = {extras = ["colors"], version = "^0.15.0"}
sphinx = "^3.2.1"
sphinx-autobuild = "^2020.9.1"
pre-commit = "^2.7.1"
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.5"
pre-commit-hooks = "^3.2.0"
sphinx-rtd-theme = "^0.5.0"
Pygments = "^2.7.1"
[tool.poetry.scripts]
python-synology = "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"

View File

@ -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

View File

@ -1,2 +0,0 @@
-r requirements.txt
-r requirements_test.txt

View File

@ -1,4 +0,0 @@
pytest
pylint>=2.6.0
pylint-strict-informational==0.1
black==20.8b1

View File

@ -1,3 +0,0 @@
[tool:pytest]
testpaths = tests
norecursedirs=.git .tox build lib

View File

@ -1,48 +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 = "1.0.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("src"),
package_dir={"": "src"},
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"],
)

View File

@ -1,5 +1,4 @@
"""DSM Storage data."""
from synology_dsm.helpers import SynoFormatHelper

View File

@ -2,7 +2,8 @@
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:

View File

@ -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:

View File

@ -1,5 +1,4 @@
"""Library constants."""
# APIs
API_INFO = "SYNO.API.Info"
API_AUTH = "SYNO.API.Auth"

View File

@ -1,8 +1,17 @@
"""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):
reason = ERROR_COMMON.get(code)
if api and not reason:
@ -25,9 +34,11 @@ class SynologyDSMException(Exception):
error_message = {"api": api, "code": code, "reason": reason, "details": details}
super().__init__(error_message)
# Request
class SynologyDSMRequestException(SynologyDSMException):
"""Request exception."""
def __init__(self, exception):
ex_class = exception.__class__.__name__
ex_reason = exception.args[0]
@ -35,26 +46,33 @@ class SynologyDSMRequestException(SynologyDSMException):
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):
super().__init__(api, -2, f"API {api} does not exists")
class SynologyDSMAPIErrorException(SynologyDSMException):
"""API returns an error exception."""
def __init__(self, api, code, details):
super().__init__(api, code, details)
# Login
class SynologyDSMLoginFailedException(SynologyDSMException):
"""Failed to login exception."""
def __init__(self, code, details=None):
super().__init__(API_AUTH, code, details)
class SynologyDSMLoginInvalidException(SynologyDSMLoginFailedException):
"""Invalid password & not admin account exception."""
def __init__(self, username):
message = f"Invalid password or not admin account: {username}"
super().__init__(400, message)
@ -62,6 +80,7 @@ class SynologyDSMLoginInvalidException(SynologyDSMLoginFailedException):
class SynologyDSMLoginDisabledAccountException(SynologyDSMLoginFailedException):
"""Guest & disabled account exception."""
def __init__(self, username):
message = f"Guest or disabled account: {username}"
super().__init__(401, message)
@ -69,6 +88,7 @@ class SynologyDSMLoginDisabledAccountException(SynologyDSMLoginFailedException):
class SynologyDSMLoginPermissionDeniedException(SynologyDSMLoginFailedException):
"""No access to login exception."""
def __init__(self, username):
message = f"Permission denied for account: {username}"
super().__init__(402, message)
@ -76,6 +96,7 @@ class SynologyDSMLoginPermissionDeniedException(SynologyDSMLoginFailedException)
class SynologyDSMLogin2SARequiredException(SynologyDSMLoginFailedException):
"""2SA required to login exception."""
def __init__(self, username):
message = f"Two-step authentication required for account: {username}"
super().__init__(403, message)
@ -83,6 +104,7 @@ class SynologyDSMLogin2SARequiredException(SynologyDSMLoginFailedException):
class SynologyDSMLogin2SAFailedException(SynologyDSMLoginFailedException):
"""2SA code failed exception."""
def __init__(self):
message = "Two-step authentication failed, retry with a new pass code"
super().__init__(404, message)

View File

@ -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.information import SynoDSMInformation
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:
@ -204,7 +202,7 @@ class SynologyDSM:
method: str,
params: dict = None,
retry_once: bool = True,
**kwargs
**kwargs,
):
"""Handles API request."""
# Discover existing APIs

View File

@ -2,69 +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
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
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.utilization import SynoCoreUtilization
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.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_CORE_SYSTEM_DS918_PLUS,
DSM_6_CORE_UPGRADE,
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: {

View File

@ -1,10 +1,8 @@
"""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

View File

@ -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 = {

View File

@ -1,26 +1,21 @@
"""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_system import (
DSM_6_CORE_SYSTEM_DS918_PLUS,
DSM_6_CORE_SYSTEM_DS218_PLAY,
)
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
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,
@ -31,22 +26,35 @@ from .download_station.const_6_download_station_task import (
from .dsm.const_6_dsm_info import DSM_6_DSM_INFORMATION
from .dsm.const_6_dsm_network import DSM_6_DSM_NETWORK
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,
)

View File

@ -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 = {

View File

@ -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 :

View File

@ -1,33 +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_HTTPS,
VALID_VERIFY_SSL,
VALID_OTP,
VALID_PASSWORD,
VALID_USER,
VALID_USER_2SA,
USER_MAX_TRY,
)
from .const import SESSION_ID, DEVICE_TOKEN, SYNO_TOKEN
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
# pylint: disable=no-self-use,protected-access

View File

@ -1,28 +1,25 @@
"""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_HTTPS,
VALID_VERIFY_SSL,
VALID_OTP,
VALID_PASSWORD,
VALID_USER,
VALID_USER_2SA,
)
from .const import SESSION_ID, DEVICE_TOKEN
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
# pylint: disable=no-self-use,protected-access,anomalous-backslash-in-string
class TestSynologyDSM(TestCase):