Files
sublime-music/sublime/adapters/adapter_base.py
2020-05-08 11:30:23 -06:00

277 lines
11 KiB
Python

import abc
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import (
Any,
Dict,
Iterable,
Optional,
Sequence,
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. If some of the data is available, but not all of it, the
``partial_data`` parameter should be set with the partial data. If the ground truth
adapter can't service the request, or errors for some reason, the UI will try to
populate itself with the partial data returned in this exception (with the necessary
error text to inform the user that retrieval from the ground truth adapter failed).
"""
def __init__(self, *args, partial_data: Any = None):
"""
Create a :class:`CacheMissError` exception.
:param args: arguments to pass to the :class:`BaseException` base class.
:param partial_data: the actual partial data for the UI to use in case of ground
truth adapter failure.
"""
self.partial_data = partial_data
super().__init__(*args)
@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
# TODO some way of specifying what types of schemas can be provided (for
# example, http, https, file)
# 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
# =========================================================================
class FunctionNames(Enum):
GET_PLAYLISTS = "get_playlists"
GET_PLAYLIST_DETAILS = "get_playlist_details"
@abc.abstractmethod
def ingest_new_data(
self,
function: "CachingAdapter.FunctionNames",
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.
"""