Skip to content

[Enhancement] Decorator-based definition of API request methods #337

@Sartohanix

Description

@Sartohanix

Context

The vast majority (all but higher-level wrappers) of methods consist in:

  • Parsing input arguments in the form of a json params dictionary
  • Updating the params dictionary with API method's name ('method' : ...) and version ('version' : ...)
  • Identifying api_path from BaseAPI.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_name used by method is clearly built in the decorator arguments (useful for documentation parsing, where currently an assignment to the variable self.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 SYNO or DSM object, leading to possible calls of the API methods through e.g. SYNO.Backup.Repository.get() and SYNO.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions