Files
sublime-music/sublime/adapters/adapter_base.py

271 lines
10 KiB
Python

import abc
from dataclasses import dataclass
from pathlib import Path
from typing import (
Any,
Dict,
Iterable,
Sequence,
Optional,
Tuple,
Type,
Union,
)
from .api_objects import (
Playlist,
PlaylistDetails,
)
class CacheMissError(Exception):
"""
This exception should be thrown by caching adapters when the request
data is not available or is invalid.
"""
pass
@dataclass
class ConfigParamDescriptor:
"""
Describes a parameter that can be used to configure an adapter. The
:class:`description`, :class:`required` and :class:`default:` should be
self-evident as to what they do.
The :class:`type` must be one of the following:
* The literal type ``str``: corresponds to a freeform text entry field in
the UI.
* The literal type ``bool``: corresponds to a checkbox in the UI.
* The literal type ``int``: corresponds to a numeric input in the UI.
* The literal string ``"password"``: corresponds to a password entry field
in the UI.
* The literal string ``"option"``: corresponds to dropdown in the UI.
The :class:`numeric_bounds` parameter only has an effect if the
:class:`type` is `int`. It specifies the min and max values that the UI
control can have.
The :class:`numeric_step` parameter only has an effect if the :class:`type`
is `int`. It specifies the step that will be taken using the "+" and "-"
buttons on the UI control (if supported).
The :class:`options` parameter only has an effect if the :class:`type` is
``"option"``. It specifies the list of options that will be available in
the dropdown in the UI.
"""
type: Union[Type, str]
description: str
required: bool = True
default: Any = None
numeric_bounds: Optional[Tuple[int, int]] = None
numeric_step: Optional[int] = None
options: Optional[Iterable[str]] = None
class Adapter(abc.ABC):
"""
Defines the interface for a Sublime Music Adapter.
All functions that actually retrieve data have a corresponding:
``can_``-prefixed property (which can be dynamic) which specifies whether
or not the adapter supports that operation at the moment.
"""
# Configuration and Initialization Properties
# These properties determine how the adapter can be configured and how to
# initialize the adapter given those configuration values.
# =========================================================================
@staticmethod
@abc.abstractmethod
def get_config_parameters() -> Dict[str, ConfigParamDescriptor]:
"""
Specifies the settings which can be configured for the adapter.
:returns: An dictionary where the keys are the name of the
configuration paramter and the values are the
:class:`ConfigParamDescriptor` object corresponding to that
configuration parameter. The order of the keys in the dictionary
correspond to the order that the configuration parameters will be
shown in the UI.
"""
@staticmethod
@abc.abstractmethod
def verify_configuration(
config: Dict[str, Any]) -> Dict[str, Optional[str]]:
"""
Specifies a function for verifying whether or not the config is valid.
:param config: The adapter configuration. The keys of are the
configuration parameter names as defined by the return value of the
:class:`get_config_parameters` function. The values are the actual
value of the configuration parameter. It is guaranteed that all
configuration parameters that are marked as required will have a
value in ``config``.
:returns: A dictionary containing varification errors. The keys of the
returned dictionary should be the same as the passed in via the
``config`` parameter. The values should be strings describing why
the corresponding value in the ``config`` dictionary is invalid.
Not all keys need be returned (for example, if there's no error for
a given configuration parameter), and returning `None` indicates no
error.
"""
@abc.abstractmethod
def __init__(self, config: dict, data_directory: Path):
"""
This function should be overridden by inheritors of
:class:`Adapter` and should be used to do whatever setup is
required for the adapter.
:param config: The adapter configuration. The keys of are the
configuration parameter names as defined by the return value of the
:class:`get_config_parameters` function. The values are the actual
value of the configuration parameter.
:param data_directory: the directory where the adapter can store data.
This directory is guaranteed to exist.
"""
def shutdown(self):
"""
This function is called when the app is being closed or the server is
changing. This should be used to clean up anything that is necessary
such as writing a cache to disk, disconnecting from a server, etc.
"""
# Usage Properties
# These properties determine how the adapter can be used and how quickly
# data can be expected from this adapter.
# =========================================================================
@property
def can_be_cached(self) -> bool:
"""
Specifies whether or not this adapter can be used as the ground-truth
adapter behind a caching adapter.
The default is ``True``, since most adapters will want to take
advantage of the built-in filesystem cache.
"""
return True
# Availability Properties
# These properties determine if what things the adapter can be used to do
# at the current moment.
# =========================================================================
@property
@abc.abstractmethod
def can_service_requests(self) -> bool:
"""
Specifies whether or not the adapter can currently service requests. If
this is ``False``, none of the other data retrieval functions are
expected to work.
For example, if your adapter requires access to an external service,
use this function to determine if it is currently possible to connect
to that external service.
"""
@property
def can_get_playlists(self) -> bool:
"""
Whether :class:`get_playlist` can be called on the adapter right now.
"""
return False
@property
def can_get_playlist_details(self) -> bool:
"""
Whether :class:`get_playlist_details` can be called on the adapter
right now.
"""
return False
# Data Retrieval Methods
# These properties determine if what things the adapter can be used to do
# at the current moment.
# =========================================================================
def get_playlists(self) -> Sequence[Playlist]:
"""
Gets a list of all of the :class:`sublime.adapter.api_objects.Playlist`
objects known to the adapter.
"""
raise self._check_can_error('get_playlists')
def get_playlist_details(
self,
playlist_id: str,
) -> PlaylistDetails:
"""
Gets the details about the given ``playlist_id``. If the playlist_id
does not exist, then this function should throw an exception.
:param playlist_id: The ID of the playlist to retrieve.
"""
raise self._check_can_error('get_playlist_details')
@staticmethod
def _check_can_error(method_name: str) -> NotImplementedError:
return NotImplementedError(
f'Adapter.{method_name} called. '
'Did you forget to check that can_{method_name} is True?')
class CachingAdapter(Adapter):
"""
Defines an adapter that can be used as a cache for another adapter.
A caching adapter sits "in front" of a non-caching adapter and the UI
will attempt to retrieve the data from the caching adapter before
retrieving it from the non-caching adapter. (The exception is when the
UI requests that the data come directly from the ground truth adapter,
in which case the cache will be bypassed.)
Caching adapters *must* be able to service requests instantly, or
nearly instantly (in most cases, this meanst the data must come
directly from the local filesystem).
"""
@abc.abstractmethod
def __init__(
self,
config: dict,
data_directory: Path,
is_cache: bool = False,
):
"""
This function should be overridden by inheritors of
:class:`CachingAdapter` and should be used to do whatever setup is
required for the adapter.
:param config: The adapter configuration. The keys of are the
configuration parameter names as defined by the return value of the
:class:`get_config_parameters` function. The values are the actual
value of the configuration parameter.
:param data_directory: the directory where the adapter can store data.
This directory is guaranteed to exist.
:param is_cache: whether or not the adapter is being used as a cache.
"""
# Data Ingestion Methods
# =========================================================================
@abc.abstractmethod
def ingest_new_data(
self,
function_name: str,
params: Tuple[Any, ...],
data: Any,
):
"""
This function will be called after the fallback, ground-truth adapter
returns new data. This normally will happen if this adapter has a cache
miss or if the UI forces retrieval from the ground-truth adapter.
:param function_name: the name of the function that was called on the
ground truth adapter.
:param params: the parameters that were passed to the function on the
ground truth adapter.
:param data: the data that was returned by the ground truth adapter.
"""