diff --git a/.travis.yml b/.travis.yml index be5944a2..c1a73ea7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,8 @@ dist: xenial language: python python: - - 2.7 - - 3.4 + - 3.6 + - 3.7 - 3.8 cache: pip: true @@ -17,7 +17,7 @@ install: - python setup.py sdist before_script: - pylint synology_dsm tests - - ./scripts/check_format.sh + - black --check --fast . script: - py.test diff --git a/README.rst b/README.rst index 9c3fc46c..ddba2dc3 100644 --- a/README.rst +++ b/README.rst @@ -190,6 +190,66 @@ Surveillance Station usage surveillance.set_home_mode(True) +System usage +-------------------------- + +.. code-block:: python + + from synology_dsm import SynologyDSM + + api = SynologyDSM("", "", "", "") + system = api.system + + # Reboot NAS + system.reboot() + + # Shutdown NAS + system.shutdown() + + # Manual update system information + system.update() + + # Get CPU information + system.cpu_clock_speed + system.cpu_cores + system.cpu_family + system.cpu_series + + # Get NTP settings + system.enabled_ntp + system.ntp_server + + # Get system information + system.firmware_ver + system.model + system.ram_size + system.serial + system.sys_temp + system.time + system.time_zone + system.time_zone_desc + system.up_time + + # Get list of all connected USB devices + system.usb_dev + + +Upgrade usage +-------------------------- + +.. code-block:: python + + from synology_dsm import SynologyDSM + + api = SynologyDSM("", "", "", "") + upgrade = api.upgrade + + # Manual update upgrade information + upgrade.update() + + # check if DSM update is available + if upgrade.update_available: + do something ... Credits / Special Thanks ======================== diff --git a/requirements.txt b/requirements.txt index 6ff91026..ca3f09a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -requests>=2.20.0 -urllib3>=1.24.3,<1.25 -six>=1.14.0 -future>=0.18.2 -simplejson>=3.16.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_test.txt b/requirements_test.txt index f15a8e3d..d69693d8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,3 +1,4 @@ pytest -pylint>=1.9.5,<=2.4.4 +pylint>=2.6.0 pylint-strict-informational==0.1 +black==20.8b1 diff --git a/scripts/check_format.sh b/scripts/check_format.sh deleted file mode 100755 index 32522cf2..00000000 --- a/scripts/check_format.sh +++ /dev/null @@ -1,16 +0,0 @@ -./scripts/common.sh - -if ! hash python3; then - echo "python3 is not installed" - exit 0 -fi - -ver=$(python3 -V 2>&1 | sed 's/.* \([0-9]\).\([0-9]\).*/\1\2/') -if [ "$ver" -lt "36" ]; then - echo "This script requires python 3.6 or greater" - exit 0 -fi - -pip install black==19.10b0 - -black --check --fast . diff --git a/setup.py b/setup.py index 058c8ccd..5f5a3411 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- +"""Synology DSM setup.""" # NOTE(ProtoThis) Guidelines for Major.Minor.Micro # - Major means an API contract change @@ -7,7 +7,6 @@ # - Micro means change of any kind (unless significant enough for a minor/major). from setuptools import setup, find_packages -from codecs import open REPO_URL = "https://github.com/ProtoThis/python-synology" VERSION = "0.9.0" @@ -25,23 +24,24 @@ download_url=REPO_URL + "/tarball/" + VERSION, description="Python API for communication with Synology DSM", long_description=long_description, - author="FG van Zeelst (ProtoThis)", + author="Quentin POLLET (Quentame) & FG van Zeelst (ProtoThis)", packages=find_packages(include=["synology_dsm*"]), install_requires=required, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + 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 :: 2.7", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "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/synology_dsm/api/core/security.py b/synology_dsm/api/core/security.py index 12ece8f0..fcc074c1 100644 --- a/synology_dsm/api/core/security.py +++ b/synology_dsm/api/core/security.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- """DSM Security data.""" -class SynoCoreSecurity(object): +class SynoCoreSecurity: """Class containing Security data.""" API_KEY = "SYNO.Core.SecurityScan.Status" diff --git a/synology_dsm/api/core/share.py b/synology_dsm/api/core/share.py index 6e38f40c..bcc5ad56 100644 --- a/synology_dsm/api/core/share.py +++ b/synology_dsm/api/core/share.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- """Shared Folders data.""" from synology_dsm.helpers import SynoFormatHelper -class SynoCoreShare(object): +class SynoCoreShare: """Class containing Share data.""" API_KEY = "SYNO.Core.Share" diff --git a/synology_dsm/api/core/system.py b/synology_dsm/api/core/system.py new file mode 100644 index 00000000..af9faa51 --- /dev/null +++ b/synology_dsm/api/core/system.py @@ -0,0 +1,117 @@ +"""DSM System data and actions.""" + + +class SynoCoreSystem: + """Class containing System data and actions.""" + + API_KEY = "SYNO.Core.System" + + def __init__(self, dsm): + self._dsm = dsm + self._data = {} + + def update(self): + """Updates System data.""" + raw_data = self._dsm.get(self.API_KEY, "info") + if raw_data: + self._data = raw_data["data"] + + ### get information + @property + def cpu_clock_speed(self): + """Gets System 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') + + @property + def cpu_family(self): + """Gets System CPU family.""" + return self._data.get('cpu_family') + + @property + def cpu_series(self): + """Gets System CPU series.""" + return self._data.get('cpu_series') + + @property + def enabled_ntp(self): + """Gets System NTP state.""" + return self._data.get('enabled_ntp') + + @property + def ntp_server(self): + """Gets System NTP server.""" + return self._data.get('ntp_server') + + @property + def firmware_ver(self): + """Gets System firmware version.""" + return self._data.get('firmware_ver') + + @property + def model(self): + """Gets System model.""" + return self._data.get('model') + + @property + def ram_size(self): + """Gets System ram size.""" + return self._data.get('ram_size') + + @property + def serial(self): + """Gets System serial number.""" + return self._data.get('serial') + + @property + def sys_temp(self): + """Gets System temperature.""" + return self._data.get('sys_temp') + + @property + def time(self): + """Gets System time.""" + return self._data.get('time') + + @property + def time_zone(self): + """Gets System 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') + + @property + def up_time(self): + """Gets System uptime.""" + return self._data.get('up_time') + + @property + def usb_dev(self): + """Gets System connected usb devices.""" + return self._data.get('usb_dev', []) + + ### 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 + ) + return res + + def reboot(self): + """Reboot NAS.""" + res = self._dsm.get( + self.API_KEY, + "reboot", + max_version=1, # reboot method is only available on api version 1 + ) + return res diff --git a/synology_dsm/api/core/upgrade.py b/synology_dsm/api/core/upgrade.py new file mode 100644 index 00000000..838444ee --- /dev/null +++ b/synology_dsm/api/core/upgrade.py @@ -0,0 +1,23 @@ +"""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/synology_dsm/api/core/utilization.py b/synology_dsm/api/core/utilization.py index d42ff874..e768c51e 100644 --- a/synology_dsm/api/core/utilization.py +++ b/synology_dsm/api/core/utilization.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- """DSM Utilization data.""" from synology_dsm.helpers import SynoFormatHelper -class SynoCoreUtilization(object): +class SynoCoreUtilization: """Class containing Utilization data.""" API_KEY = "SYNO.Core.System.Utilization" diff --git a/synology_dsm/api/download_station/__init__.py b/synology_dsm/api/download_station/__init__.py index 13d7dce1..94866dd5 100644 --- a/synology_dsm/api/download_station/__init__.py +++ b/synology_dsm/api/download_station/__init__.py @@ -2,7 +2,7 @@ from .task import SynoDownloadTask -class SynoDownloadStation(object): +class SynoDownloadStation: """An implementation of a Synology DownloadStation.""" API_KEY = "SYNO.DownloadStation.*" diff --git a/synology_dsm/api/download_station/task.py b/synology_dsm/api/download_station/task.py index 7a07561b..b4b6a6cb 100644 --- a/synology_dsm/api/download_station/task.py +++ b/synology_dsm/api/download_station/task.py @@ -1,7 +1,7 @@ """DownloadStation task.""" -class SynoDownloadTask(object): +class SynoDownloadTask: """An representation of a Synology DownloadStation task.""" def __init__(self, data): diff --git a/synology_dsm/api/dsm/information.py b/synology_dsm/api/dsm/information.py index 0c3a6b24..7c73cc9e 100644 --- a/synology_dsm/api/dsm/information.py +++ b/synology_dsm/api/dsm/information.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- """DSM Information data.""" -class SynoDSMInformation(object): +class SynoDSMInformation: """Class containing Information data.""" API_KEY = "SYNO.DSM.Info" diff --git a/synology_dsm/api/dsm/network.py b/synology_dsm/api/dsm/network.py index 7a214025..451d111c 100644 --- a/synology_dsm/api/dsm/network.py +++ b/synology_dsm/api/dsm/network.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- """DSM Network data.""" -class SynoDSMNetwork(object): +class SynoDSMNetwork: """Class containing Network data.""" API_KEY = "SYNO.DSM.Network" diff --git a/synology_dsm/api/storage/storage.py b/synology_dsm/api/storage/storage.py index 0eb9d3b2..ed2ce661 100644 --- a/synology_dsm/api/storage/storage.py +++ b/synology_dsm/api/storage/storage.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- """DSM Storage data.""" -from __future__ import division from synology_dsm.helpers import SynoFormatHelper -class SynoStorage(object): +class SynoStorage: """Class containing Storage data.""" API_KEY = "SYNO.Storage.CGI.Storage" diff --git a/synology_dsm/api/surveillance_station/__init__.py b/synology_dsm/api/surveillance_station/__init__.py index 112d3797..047284de 100644 --- a/synology_dsm/api/surveillance_station/__init__.py +++ b/synology_dsm/api/surveillance_station/__init__.py @@ -5,7 +5,7 @@ from .const import MOTION_DETECTION_BY_SURVEILLANCE, MOTION_DETECTION_DISABLED -class SynoSurveillanceStation(object): +class SynoSurveillanceStation: """An implementation of a Synology SurveillanceStation.""" API_KEY = "SYNO.SurveillanceStation.*" @@ -37,12 +37,12 @@ def update(self): )["data"] ) - live_view_data = self._dsm.get( + live_view_datas = self._dsm.get( self.CAMERA_API_KEY, "GetLiveViewPath", {"idList": ",".join(str(k) for k in self._cameras_by_id)}, )["data"] - for live_view_data in live_view_data: + for live_view_data in live_view_datas: self._cameras_by_id[live_view_data["id"]].live_view.update(live_view_data) # Global diff --git a/synology_dsm/api/surveillance_station/camera.py b/synology_dsm/api/surveillance_station/camera.py index 5096a03f..0c32812e 100644 --- a/synology_dsm/api/surveillance_station/camera.py +++ b/synology_dsm/api/surveillance_station/camera.py @@ -2,7 +2,7 @@ from .const import RECORDING_STATUS, MOTION_DETECTION_DISABLED -class SynoCamera(object): +class SynoCamera: """An representation of a Synology SurveillanceStation camera.""" def __init__(self, data, live_view_data=None): @@ -62,7 +62,7 @@ def is_recording(self): return self._data["recStatus"] in RECORDING_STATUS -class SynoCameraLiveView(object): +class SynoCameraLiveView: """An representation of a Synology SurveillanceStation camera live view.""" def __init__(self, data): diff --git a/synology_dsm/const.py b/synology_dsm/const.py index a471e452..9d4609b9 100644 --- a/synology_dsm/const.py +++ b/synology_dsm/const.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Library constants.""" # APIs diff --git a/synology_dsm/exceptions.py b/synology_dsm/exceptions.py index 377d40f7..799fdc99 100644 --- a/synology_dsm/exceptions.py +++ b/synology_dsm/exceptions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Library exceptions.""" from .const import API_AUTH, ERROR_AUTH, ERROR_COMMON, ERROR_DOWNLOAD_SEARCH, ERROR_DOWNLOAD_TASK, ERROR_FILE, ERROR_SURVEILLANCE, ERROR_VIRTUALIZATION @@ -24,7 +23,7 @@ def __init__(self, api, code, details=None): reason = "Unknown" error_message={"api": api, "code": code, "reason": reason, "details": details} - super(SynologyDSMException, self).__init__(error_message) + super().__init__(error_message) # Request class SynologyDSMRequestException(SynologyDSMException): @@ -34,56 +33,56 @@ def __init__(self, exception): ex_reason = exception.args[0] if hasattr(exception.args[0], "reason"): ex_reason = exception.args[0].reason - message = "%s = %s" % (ex_class, ex_reason) - super(SynologyDSMRequestException, self).__init__(None, -1, message) + super().__init__(None, -1, f"{ex_class} = {ex_reason}") # API class SynologyDSMAPINotExistsException(SynologyDSMException): """API not exists exception.""" def __init__(self, api): - super(SynologyDSMAPINotExistsException, self).__init__(api, -2, "API %s does not exists" % 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(SynologyDSMAPIErrorException, self).__init__(api, code, details) + super().__init__(api, code, details) # Login class SynologyDSMLoginFailedException(SynologyDSMException): """Failed to login exception.""" - pass + 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 = "Invalid password or not admin account: %s" % username - super(SynologyDSMLoginInvalidException, self).__init__(API_AUTH, 400, message) + message = f"Invalid password or not admin account: {username}" + super().__init__(400, message) class SynologyDSMLoginDisabledAccountException(SynologyDSMLoginFailedException): """Guest & disabled account exception.""" def __init__(self, username): - message = "Guest or disabled account: %s" % username - super(SynologyDSMLoginDisabledAccountException, self).__init__(API_AUTH, 401, message) + message = f"Guest or disabled account: {username}" + super().__init__(401, message) class SynologyDSMLoginPermissionDeniedException(SynologyDSMLoginFailedException): """No access to login exception.""" def __init__(self, username): - message = "Permission denied for account: %s" % username - super(SynologyDSMLoginPermissionDeniedException, self).__init__(API_AUTH, 402, message) + message = f"Permission denied for account: {username}" + super().__init__(402, message) class SynologyDSMLogin2SARequiredException(SynologyDSMLoginFailedException): """2SA required to login exception.""" def __init__(self, username): - message = "Two-step authentication required for account: %s" % username - super(SynologyDSMLogin2SARequiredException, self).__init__(API_AUTH, 403, message) + message = f"Two-step authentication required for account: {username}" + super().__init__(403, message) class SynologyDSMLogin2SAFailedException(SynologyDSMLoginFailedException): """2SA code failed exception.""" def __init__(self): message = "Two-step authentication failed, retry with a new pass code" - super(SynologyDSMLogin2SAFailedException, self).__init__(API_AUTH, 404, message) + super().__init__(404, message) diff --git a/synology_dsm/helpers.py b/synology_dsm/helpers.py index e7647f45..8098771e 100644 --- a/synology_dsm/helpers.py +++ b/synology_dsm/helpers.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- """Helpers.""" -class SynoFormatHelper(object): +class SynoFormatHelper: """Class containing various formatting functions.""" @staticmethod diff --git a/synology_dsm/synology_dsm.py b/synology_dsm/synology_dsm.py index 71cc4b44..87f09d54 100644 --- a/synology_dsm/synology_dsm.py +++ b/synology_dsm/synology_dsm.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- """Class to interact with Synology DSM.""" +from json import JSONDecodeError +from urllib.parse import quote import socket + import urllib3 -import six from requests import Session from requests.exceptions import RequestException -from simplejson.errors import JSONDecodeError from .exceptions import ( SynologyDSMAPIErrorException, @@ -20,8 +20,10 @@ ) from .api.core.security import SynoCoreSecurity -from .api.core.utilization import SynoCoreUtilization from .api.core.share import SynoCoreShare +from .api.core.system import SynoCoreSystem +from .api.core.upgrade import SynoCoreUpgrade +from .api.core.utilization import SynoCoreUtilization from .api.download_station import SynoDownloadStation from .api.dsm.information import SynoDSMInformation from .api.dsm.network import SynoDSMNetwork @@ -29,13 +31,8 @@ from .api.surveillance_station import SynoSurveillanceStation from .const import API_AUTH, API_INFO -if six.PY2: - from future.moves.urllib.parse import quote -else: - from urllib.parse import quote # pylint: disable=import-error,no-name-in-module - -class SynologyDSM(object): +class SynologyDSM: """Class containing the main Synology DSM functions.""" DSM_5_WEIRD_URL_API = [ @@ -44,14 +41,14 @@ class SynologyDSM(object): def __init__( self, - dsm_ip, - dsm_port, - username, - password, - use_https=False, - timeout=None, - device_token=None, - debugmode=False, + dsm_ip: str, + dsm_port: int, + username: str, + password: str, + use_https: bool = False, + timeout: int = None, + device_token: str = None, + debugmode: bool = False, ): self.username = username self._password = password @@ -75,10 +72,12 @@ def __init__( self._information = None self._network = None self._security = None - self._utilisation = None - self._storage = None self._share = None + self._storage = None self._surveillance = None + self._system = None + self._utilisation = None + self._upgrade = None # Build variables if use_https: @@ -86,16 +85,16 @@ def __init__( # disable SSL warnings due to the auto-genenerated cert urllib3.disable_warnings() - self._base_url = "https://%s:%s" % (dsm_ip, dsm_port) + self._base_url = f"https://{dsm_ip}:{dsm_port}" else: - self._base_url = "http://%s:%s" % (dsm_ip, dsm_port) + self._base_url = f"http://{dsm_ip}:{dsm_port}" - def _debuglog(self, message): + def _debuglog(self, message: str): """Outputs message if debug mode is enabled.""" if self._debugmode: print("DEBUG: " + message) - def _is_weird_api_url(self, api): + 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].""" return ( api in self.DSM_5_WEIRD_URL_API @@ -104,15 +103,12 @@ def _is_weird_api_url(self, api): and int(self._information.version) < 7321 # < DSM 6 ) - def _build_url(self, api): + def _build_url(self, api: str) -> str: if self._is_weird_api_url(api): if api == SynoStorage.API_KEY: - return ( - "%s/webman/modules/StorageManager/storagehandler.cgi?" - % self._base_url - ) + return f"{self._base_url}/webman/modules/StorageManager/storagehandler.cgi?" - return "%s/webapi/%s?" % (self._base_url, self.apis[api]["path"]) + return f"{self._base_url}/webapi/{self.apis[api]['path']}?" def discover_apis(self): """Retreives available API infos from the NAS.""" @@ -125,7 +121,7 @@ def apis(self): """Gets available API infos from the NAS.""" return self._apis - def login(self, otp_code=None): + def login(self, otp_code: str = None): """Create a logged session.""" # First reset the session self._debuglog("Creating new session") @@ -156,9 +152,12 @@ def login(self, otp_code=None): 401: SynologyDSMLoginDisabledAccountException(self.username), 402: SynologyDSMLoginPermissionDeniedException(self.username), 403: SynologyDSMLogin2SARequiredException(self.username), - 404: SynologyDSMLogin2SAFailedException, + 404: SynologyDSMLogin2SAFailedException(), } - raise switcher.get(result["error"]["code"], SynologyDSMLoginFailedException) + raise switcher.get( + result["error"]["code"], + SynologyDSMLoginFailedException(result["error"]["code"], self.username), + ) # Parse result if valid self._session_id = result["data"]["sid"] @@ -177,20 +176,26 @@ def login(self, otp_code=None): return True @property - def device_token(self): + def device_token(self) -> str: """Gets the device token to remember the 2SA access was granted on this device.""" return self._device_token - def get(self, api, method, params=None, **kwargs): + def get(self, api: str, method: str, params: dict = None, **kwargs): """Handles API GET request.""" return self._request("GET", api, method, params, **kwargs) - def post(self, api, method, params=None, **kwargs): + def post(self, api: str, method: str, params: dict = None, **kwargs): """Handles API POST request.""" return self._request("POST", api, method, params, **kwargs) def _request( - self, request_method, api, method, params=None, retry_once=True, **kwargs + self, + request_method: str, + api: str, + method: str, + params: dict = None, + retry_once: bool = True, + **kwargs ): """Handles API request.""" # Discover existing APIs @@ -249,17 +254,13 @@ def _request( return response - def _execute_request(self, method, url, params, **kwargs): + def _execute_request(self, method: str, url: str, params: dict, **kwargs): """Function to execute and handle a request.""" # Execute Request try: if method == "GET": - if six.PY2: - items = params.iteritems() - else: - items = params.items() encoded_params = "&".join( - "%s=%s" % (key, quote(str(value))) for key, value in items + f"{key}={quote(str(value))}" for key, value in params.items() ) response = self._session.get( url, params=encoded_params, timeout=self._timeout, **kwargs @@ -297,9 +298,9 @@ def _execute_request(self, method, url, params, **kwargs): raise RequestException(response) except (RequestException, JSONDecodeError) as exp: - raise SynologyDSMRequestException(exp) + raise SynologyDSMRequestException(exp) from exp - def update(self, with_information=False, with_network=False): + def update(self, with_information: bool = False, with_network: bool = False): """Updates the various instanced modules.""" if self._download: self._download.update() @@ -325,7 +326,13 @@ def update(self, with_information=False, with_network=False): if self._surveillance: self._surveillance.update() - def reset(self, api): + if self._system: + self._system.update() + + if self._upgrade: + self._upgrade.update() + + def reset(self, api: any) -> bool: """Reset an API to avoid fetching in on update.""" if isinstance(api, str): if api in ("information", SynoDSMInformation.API_KEY): @@ -333,97 +340,122 @@ def reset(self, api): if hasattr(self, "_" + api): setattr(self, "_" + api, None) return True - if api == SynoDownloadStation.API_KEY: - self._download = None - return True if api == SynoCoreSecurity.API_KEY: self._security = None return True if api == SynoCoreShare.API_KEY: self._share = None return True + if api == SynoCoreSystem.API_KEY: + self._system = None + return True + if api == SynoCoreUpgrade.API_KEY: + self._upgrade = None + return True if api == SynoCoreUtilization.API_KEY: self._utilisation = None return True + if api == SynoDownloadStation.API_KEY: + self._download = None + return True if api == SynoStorage.API_KEY: self._storage = None return True if api == SynoSurveillanceStation.API_KEY: self._surveillance = None return True - if isinstance(api, SynoDownloadStation): - self._download = None - return True if isinstance(api, SynoCoreSecurity): self._security = None return True if isinstance(api, SynoCoreShare): self._share = None return True + if isinstance(api, SynoCoreSystem): + self._system = None + return True + if isinstance(api, SynoCoreUpgrade): + self._utilisation = None + return True if isinstance(api, SynoCoreUtilization): self._utilisation = None return True + if isinstance(api, SynoDownloadStation): + self._download = None + return True if isinstance(api, SynoStorage): self._storage = None return True - if isinstance(api, SynoSurveillanceStation): self._surveillance = None return True return False @property - def download_station(self): + def download_station(self) -> SynoDownloadStation: """Gets NAS DownloadStation.""" if not self._download: self._download = SynoDownloadStation(self) return self._download @property - def information(self): + def information(self) -> SynoDSMInformation: """Gets NAS informations.""" if not self._information: self._information = SynoDSMInformation(self) return self._information @property - def network(self): + def network(self) -> SynoDSMNetwork: """Gets NAS network informations.""" if not self._network: self._network = SynoDSMNetwork(self) return self._network @property - def security(self): + def security(self) -> SynoCoreSecurity: """Gets NAS security informations.""" if not self._security: self._security = SynoCoreSecurity(self) return self._security @property - def utilisation(self): - """Gets NAS utilisation informations.""" - if not self._utilisation: - self._utilisation = SynoCoreUtilization(self) - return self._utilisation + def share(self) -> SynoCoreShare: + """Gets NAS shares information.""" + if not self._share: + self._share = SynoCoreShare(self) + return self._share @property - def storage(self): + def storage(self) -> SynoStorage: """Gets NAS storage informations.""" if not self._storage: self._storage = SynoStorage(self) return self._storage @property - def share(self): - """Gets NAS shares information.""" - if not self._share: - self._share = SynoCoreShare(self) - return self._share - - @property - def surveillance_station(self): + def surveillance_station(self) -> SynoSurveillanceStation: """Gets NAS SurveillanceStation.""" if not self._surveillance: self._surveillance = SynoSurveillanceStation(self) return self._surveillance + + @property + def system(self) -> SynoCoreSystem: + """Gets NAS system information.""" + if not self._system: + self._system = SynoCoreSystem(self) + return self._system + + @property + def upgrade(self) -> SynoCoreUpgrade: + """Gets NAS upgrade informations.""" + if not self._upgrade: + self._upgrade = SynoCoreUpgrade(self) + return self._upgrade + + @property + def utilisation(self) -> SynoCoreUtilization: + """Gets NAS utilisation informations.""" + if not self._utilisation: + self._utilisation = SynoCoreUtilization(self) + return self._utilisation diff --git a/tests/__init__.py b/tests/__init__.py index be29abf3..a7356fbe 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- """Library tests.""" -import six +from json import JSONDecodeError +from urllib.parse import urlencode + from requests.exceptions import ConnectionError as ConnError, RequestException, SSLError -from simplejson.errors import JSONDecodeError from synology_dsm import SynologyDSM from synology_dsm.exceptions import SynologyDSMRequestException @@ -19,6 +19,7 @@ from .const import ( ERROR_INSUFFICIENT_USER_PRIVILEGE, ERROR_AUTH_INVALID_CREDENTIALS, + ERROR_AUTH_MAX_TRIES, ERROR_AUTH_OTP_AUTHENTICATE_FAILED, DEVICE_TOKEN, ) @@ -70,7 +71,9 @@ "DSM_INFORMATION": DSM_5_DSM_INFORMATION, "DSM_NETWORK": DSM_5_DSM_NETWORK, "CORE_UTILIZATION": DSM_5_CORE_UTILIZATION, - "STORAGE_STORAGE": {"RAID": DSM_5_STORAGE_STORAGE_DS410J_RAID5_4DISKS_1VOL,}, + "STORAGE_STORAGE": { + "RAID": DSM_5_STORAGE_STORAGE_DS410J_RAID5_4DISKS_1VOL, + }, }, 6: { "API_INFO": DSM_6_API_INFO, @@ -92,11 +95,6 @@ } -if six.PY2: - from future.moves.urllib.parse import urlencode -else: - from urllib.parse import urlencode # pylint: disable=import-error,no-name-in-module - VALID_HOST = "nas.mywebsite.me" VALID_PORT = "443" VALID_SSL = True @@ -105,6 +103,8 @@ VALID_PASSWORD = "valid_password" VALID_OTP = "123456" +USER_MAX_TRY = "user_max" + class SynologyDSMMock(SynologyDSM): """Mocked SynologyDSM.""" @@ -158,7 +158,7 @@ def _execute_request(self, method, url, params, **kwargs): if VALID_PORT not in url and "https" not in url: raise SynologyDSMRequestException( - JSONDecodeError("Expecting value", "document", 0, None) + JSONDecodeError("Expecting value", "document", 0) ) if VALID_PORT not in url: @@ -192,6 +192,9 @@ def _execute_request(self, method, url, params, **kwargs): if VALID_USER in url and VALID_PASSWORD in url: return API_SWITCHER[self.dsm_version]["AUTH_LOGIN"] + if USER_MAX_TRY in url: + return ERROR_AUTH_MAX_TRIES + return ERROR_AUTH_INVALID_CREDENTIALS if self.API_URI in url: diff --git a/tests/api_data/dsm_5/core/const_5_core_utilization.py b/tests/api_data/dsm_5/core/const_5_core_utilization.py index 94b201e0..9abce7da 100644 --- a/tests/api_data/dsm_5/core/const_5_core_utilization.py +++ b/tests/api_data/dsm_5/core/const_5_core_utilization.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 5 SYNO.Core.System.Utilization data.""" DSM_5_CORE_UTILIZATION = { diff --git a/tests/api_data/dsm_5/dsm/const_5_dsm_info.py b/tests/api_data/dsm_5/dsm/const_5_dsm_info.py index 453bc8cb..6fd6d059 100644 --- a/tests/api_data/dsm_5/dsm/const_5_dsm_info.py +++ b/tests/api_data/dsm_5/dsm/const_5_dsm_info.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 5 SYNO.DSM.Info data.""" DSM_5_DSM_INFORMATION_DS410J = { diff --git a/tests/api_data/dsm_5/dsm/const_5_dsm_network.py b/tests/api_data/dsm_5/dsm/const_5_dsm_network.py index 98e272bc..2ab5f3cb 100644 --- a/tests/api_data/dsm_5/dsm/const_5_dsm_network.py +++ b/tests/api_data/dsm_5/dsm/const_5_dsm_network.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 5 SYNO.DSM.Network data.""" DSM_5_DSM_NETWORK = { diff --git a/tests/api_data/dsm_5/storage/const_5_storage_storage.py b/tests/api_data/dsm_5/storage/const_5_storage_storage.py index 674fed66..9af04897 100644 --- a/tests/api_data/dsm_5/storage/const_5_storage_storage.py +++ b/tests/api_data/dsm_5/storage/const_5_storage_storage.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 5 SYNO.Storage.CGI.Storage data.""" from tests.const import UNIQUE_KEY @@ -253,10 +252,18 @@ "vol_path": "/volume1", "vspace_can_do": { "drbd": { - "resize": {"can_do": False, "errCode": 53504, "stopService": False,} + "resize": { + "can_do": False, + "errCode": 53504, + "stopService": False, + } }, "flashcache": { - "apply": {"can_do": False, "errCode": 53504, "stopService": False,}, + "apply": { + "can_do": False, + "errCode": 53504, + "stopService": False, + }, "remove": { "can_do": False, "errCode": 53504, diff --git a/tests/api_data/dsm_6/core/const_6_core_security.py b/tests/api_data/dsm_6/core/const_6_core_security.py index 10339865..e695887d 100644 --- a/tests/api_data/dsm_6/core/const_6_core_security.py +++ b/tests/api_data/dsm_6/core/const_6_core_security.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.Core.SecurityScan.Status data.""" DSM_6_CORE_SECURITY = { diff --git a/tests/api_data/dsm_6/core/const_6_core_share.py b/tests/api_data/dsm_6/core/const_6_core_share.py index 08f37662..fe663582 100644 --- a/tests/api_data/dsm_6/core/const_6_core_share.py +++ b/tests/api_data/dsm_6/core/const_6_core_share.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.Core.Share data.""" DSM_6_CORE_SHARE = { 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 new file mode 100644 index 00000000..2073e436 --- /dev/null +++ b/tests/api_data/dsm_6/core/const_6_core_system.py @@ -0,0 +1,38 @@ +"""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 +} 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 new file mode 100644 index 00000000..8b393ed0 --- /dev/null +++ b/tests/api_data/dsm_6/core/const_6_core_upgrade.py @@ -0,0 +1,10 @@ +"""DSM 6 SYNO.Core.Upgrade data.""" + +DSM_6_CORE_SYSTEM = { + "data":{ + "update":{ + "available":False + } + }, + "success":True +} diff --git a/tests/api_data/dsm_6/core/const_6_core_utilization.py b/tests/api_data/dsm_6/core/const_6_core_utilization.py index 490aacfa..dac2678c 100644 --- a/tests/api_data/dsm_6/core/const_6_core_utilization.py +++ b/tests/api_data/dsm_6/core/const_6_core_utilization.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.Core.System.Utilization data.""" DSM_6_CORE_UTILIZATION_ERROR_1055 = { diff --git a/tests/api_data/dsm_6/download_station/const_6_download_station_info.py b/tests/api_data/dsm_6/download_station/const_6_download_station_info.py index 9ef80a7e..5f8fadb5 100644 --- a/tests/api_data/dsm_6/download_station/const_6_download_station_info.py +++ b/tests/api_data/dsm_6/download_station/const_6_download_station_info.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.DownloadStation.Info data.""" DSM_6_DOWNLOAD_STATION_INFO_INFO = { diff --git a/tests/api_data/dsm_6/download_station/const_6_download_station_stat.py b/tests/api_data/dsm_6/download_station/const_6_download_station_stat.py index b5f8c004..34a6ef61 100644 --- a/tests/api_data/dsm_6/download_station/const_6_download_station_stat.py +++ b/tests/api_data/dsm_6/download_station/const_6_download_station_stat.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.DownloadStation.Statistic data.""" DSM_6_DOWNLOAD_STATION_STAT_INFO = { diff --git a/tests/api_data/dsm_6/download_station/const_6_download_station_task.py b/tests/api_data/dsm_6/download_station/const_6_download_station_task.py index 1c7b7179..180c87c8 100644 --- a/tests/api_data/dsm_6/download_station/const_6_download_station_task.py +++ b/tests/api_data/dsm_6/download_station/const_6_download_station_task.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.DownloadStation.Task data.""" DSM_6_DOWNLOAD_STATION_TASK_LIST = { diff --git a/tests/api_data/dsm_6/dsm/const_6_dsm_info.py b/tests/api_data/dsm_6/dsm/const_6_dsm_info.py index c20191a0..f43f9a62 100644 --- a/tests/api_data/dsm_6/dsm/const_6_dsm_info.py +++ b/tests/api_data/dsm_6/dsm/const_6_dsm_info.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.DSM.Info data.""" DSM_6_DSM_INFORMATION_DS213_PLUS = { 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 48304626..38a6d05f 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,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.DSM.Network data.""" DSM_6_DSM_NETWORK = { diff --git a/tests/api_data/dsm_6/storage/const_6_storage_storage.py b/tests/api_data/dsm_6/storage/const_6_storage_storage.py index 6535b9a8..a86913e1 100644 --- a/tests/api_data/dsm_6/storage/const_6_storage_storage.py +++ b/tests/api_data/dsm_6/storage/const_6_storage_storage.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.Storage.CGI.Storage data.""" from tests.const import UNIQUE_KEY @@ -1979,7 +1978,10 @@ "env": { "batchtask": {"max_task": 64, "remain_task": 64}, "bay_number": "4", - "data_scrubbing": {"sche_enabled": "0", "sche_status": "disabled",}, + "data_scrubbing": { + "sche_enabled": "0", + "sche_status": "disabled", + }, "ebox": [], "fs_acting": False, "isSyncSysPartition": False, @@ -1993,8 +1995,15 @@ "ram_size": 4, "ram_size_required": 32, "showpooltab": False, - "status": {"system_crashed": False, "system_need_repair": False,}, - "support": {"ebox": True, "raid_cross": True, "sysdef": True,}, + "status": { + "system_crashed": False, + "system_need_repair": False, + }, + "support": { + "ebox": True, + "raid_cross": True, + "sysdef": True, + }, "support_fit_fs_limit": True, "unique_key": UNIQUE_KEY, "volume_full_critical": 0.1, @@ -2040,9 +2049,21 @@ { "designedDiskCount": 3, "devices": [ - {"id": "sdc", "slot": 2, "status": "normal",}, - {"id": "sdb", "slot": 1, "status": "normal",}, - {"id": "sda", "slot": 0, "status": "normal",}, + { + "id": "sdc", + "slot": 2, + "status": "normal", + }, + { + "id": "sdb", + "slot": 1, + "status": "normal", + }, + { + "id": "sda", + "slot": 0, + "status": "normal", + }, ], "hasParity": True, "minDevSize": "4000681164800", @@ -2053,7 +2074,10 @@ } ], "scrubbingStatus": "no_action", - "size": {"total": "7991698522112", "used": "7991698522112",}, + "size": { + "total": "7991698522112", + "used": "7991698522112", + }, "space_path": "/dev/md2", "ssd_trim": {"support": "not support"}, "status": "normal", @@ -2068,9 +2092,21 @@ } }, "flashcache": { - "apply": {"can_do": True, "errCode": 0, "stopService": True,}, - "remove": {"can_do": True, "errCode": 0, "stopService": False,}, - "resize": {"can_do": True, "errCode": 0, "stopService": False,}, + "apply": { + "can_do": True, + "errCode": 0, + "stopService": True, + }, + "remove": { + "can_do": True, + "errCode": 0, + "stopService": False, + }, + "resize": { + "can_do": True, + "errCode": 0, + "stopService": False, + }, }, "snapshot": { "resize": { @@ -2142,9 +2178,21 @@ } }, "flashcache": { - "apply": {"can_do": True, "errCode": 0, "stopService": True,}, - "remove": {"can_do": True, "errCode": 0, "stopService": False,}, - "resize": {"can_do": True, "errCode": 0, "stopService": False,}, + "apply": { + "can_do": True, + "errCode": 0, + "stopService": True, + }, + "remove": { + "can_do": True, + "errCode": 0, + "stopService": False, + }, + "resize": { + "can_do": True, + "errCode": 0, + "stopService": False, + }, }, "snapshot": { "resize": { diff --git a/tests/api_data/dsm_6/surveillance_station/const_6_surveillance_station_camera.py b/tests/api_data/dsm_6/surveillance_station/const_6_surveillance_station_camera.py index e2187bd1..d39848b8 100644 --- a/tests/api_data/dsm_6/surveillance_station/const_6_surveillance_station_camera.py +++ b/tests/api_data/dsm_6/surveillance_station/const_6_surveillance_station_camera.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.SurveillanceStation.Camera data.""" DSM_6_SURVEILLANCE_STATION_CAMERA_LIST = { diff --git a/tests/api_data/dsm_6/surveillance_station/const_6_surveillance_station_home_mode.py b/tests/api_data/dsm_6/surveillance_station/const_6_surveillance_station_home_mode.py index 1b7afde5..b58735bc 100644 --- a/tests/api_data/dsm_6/surveillance_station/const_6_surveillance_station_home_mode.py +++ b/tests/api_data/dsm_6/surveillance_station/const_6_surveillance_station_home_mode.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """DSM 6 SYNO.API.SurveillanceStation.HomeMode data.""" DSM_6_SURVEILLANCE_STATION_HOME_MODE_GET_INFO = { diff --git a/tests/const.py b/tests/const.py index c5466528..6e938f53 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Test constants.""" # API test data are localized in diff --git a/tests/test_synology_dsm.py b/tests/test_synology_dsm.py index 1461e49b..87071b24 100644 --- a/tests/test_synology_dsm.py +++ b/tests/test_synology_dsm.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Synology DSM tests.""" from unittest import TestCase import pytest @@ -12,6 +11,7 @@ SynologyDSMLoginInvalidException, SynologyDSMLogin2SARequiredException, SynologyDSMLogin2SAFailedException, + SynologyDSMLoginFailedException, ) from synology_dsm.const import API_AUTH, API_INFO @@ -24,6 +24,7 @@ VALID_PASSWORD, VALID_USER, VALID_USER_2SA, + USER_MAX_TRY, ) from .const import SESSION_ID, DEVICE_TOKEN, SYNO_TOKEN @@ -222,6 +223,20 @@ def test_login_2sa_failed(self): assert api._syno_token is None assert api._device_token is None + def test_login_basic_failed(self): + """Test basic failed login.""" + api = SynologyDSMMock( + VALID_HOST, VALID_PORT, USER_MAX_TRY, VALID_PASSWORD, VALID_SSL + ) + + with pytest.raises(SynologyDSMLoginFailedException) as error: + api.login() + error_value = error.value.args[0] + assert error_value["api"] == "SYNO.API.Auth" + assert error_value["code"] == 407 + assert error_value["reason"] == "Max Tries (if auto blocking is set to true)" + assert error_value["details"] == USER_MAX_TRY + def test_request_timeout(self): """Test request timeout.""" api = SynologyDSMMock( diff --git a/tests/test_synology_dsm_5.py b/tests/test_synology_dsm_5.py index c1141357..ee0577c7 100644 --- a/tests/test_synology_dsm_5.py +++ b/tests/test_synology_dsm_5.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Synology DSM tests.""" from unittest import TestCase