-
-
Notifications
You must be signed in to change notification settings - Fork 164
Description
Context
The vast majority (all but higher-level wrappers) of methods consist in:
- Parsing input arguments in the form of a json
paramsdictionary - Updating the
paramsdictionary with API method's name ('method' : ...) and version ('version' : ...) - Identifying
api_pathfromBaseAPI.gen_list[api_name]['path'] - Returning
BaseAPI.request_data(api_name, api_path, params)
Of these steps, only the first one really is unique to each API method. The last three are redundant and could be automatised. Here is a suggestion of improvement based on decorated methods.
Suggestion
Let's consider the following canonical example, from file core_backup.py:
class Backup(base_api.BaseApi):
"""Synology Hyper Backup API."""
def backup_repository_get(self, task_id: str) -> dict[str, object] | str:
api_name = 'SYNO.Backup.Repository'
info = self.gen_list[api_name]
api_path = info['path']
req_param = {'version': info['minVersion'],
'method': 'get', 'task_id': task_id}
return self.request_data(api_name, api_path, req_param)The decorated implementation of method backup_repository_get() would look like:
class Backup(base_api.BaseApi):
"""Synology Hyper Backup API."""
@api(name='SYNO.Backup.Repository', method='get')
def backup_repository_get(self, task_id: str) -> dict[str, object]:
return {'task_id': task_id}And similarly for other methods. One could imagine cases where the name=<api_name> and method=<method_name> arguments would not need to be provided. Respectively when the static class attribute _API_NAME = '...' is provided and assumed to be the default API name for methods of that class ; and when the name of the python method unambiguously matches the name of the SYNO API method. Here is a possible example from file core_iscsi.py:
class LUN(base_api.BaseApi):
_API_NAME = "SYNO.Core.ISCSI.LUN"
# In the @api decorator below,
# -> Default api name = self._API_NAME = "SYNO.Core.ISCSI.LUN"
# -> Default method name = 'delete'
@api
def delete(self, uuid_or_uuids_list: str | Sequence[str]) -> dict:
return {
"uuid": '""',
"uuids": _json(_ensure_list(uuid_or_uuids_list))
}Advantages
There are multiple advantages in using such a decorator implementation for API methods:
- Redundant building of meta-arguments for requests in systematised in some common framework (
version,api_path). The decorator is defined once and imported in each file. - Internal
api_nameused by method is clearly built in the decorator arguments (useful for documentation parsing, where currently an assignment to the variableself.api_name = '...'is enforced) - The distinction between API-request (decorated) and utility methods (non-decorated) is unambiguous
- The decorator mechanism could be used to register methods in a single common class/object, as suggested in Reorganization of the wrapper by apps #259, at class-import time. For instance, importing the classes from the examples above could dynamically populate a common
SYNOorDSMobject, leading to possible calls of the API methods through e.g.SYNO.Backup.Repository.get()andSYNO.Core.ISCSI.LUN.delete(). This reconciles the currently implemented approach and the reorganisation of methods per API path suggested in Reorganization of the wrapper by apps #259 ; while maintaining full backward compatibility.