REFACTOR: Make registry & tag handling simpler

This commit is contained in:
Dominique Barton 2020-02-09 00:23:06 +01:00
parent 022db5998f
commit 68cce201b0
9 changed files with 286 additions and 270 deletions

View file

@ -11,46 +11,62 @@ import os
import json
from logging import getLogger
from mopidy_pummeluff import tags
LOGGER = getLogger(__name__)
class RegistryDict(dict):
'''
Simple tag registry based on Python's internal :py:class:`dict` class,
which reads and writes the registry from/to disk.
Class which can be used to retreive and write RFID tags to the registry.
'''
registry_path = '/var/lib/mopidy/pummeluff/tags.json'
def __init__(self):
super(RegistryDict, self).__init__(self)
'''
Constructor.
Automatically reads the registry if it exists.
'''
super().__init__()
if os.path.exists(self.registry_path):
self.read()
else:
LOGGER.warning('Registry not existing yet on "%s"', self.registry_path)
def __getitem__(self, key):
return super(RegistryDict, self).__getitem__(str(key))
def __setitem__(self, key, item):
super(RegistryDict, self).__setitem__(str(key), item)
self.write()
def get(self, key, default=None):
@classmethod
def unserialize_item(cls, item):
'''
Return the value for ``key`` if ``key`` is in the dictionary, else
``default``.
Unserialize an item from the persistent storage on filesystem to a
native tag.
:param str key: The key
:param default: The default value
:type default: mixed
:param tuple item: The item
:return: The value
:rtype: mixed
:return: The tag
:rtype: tags.tag
'''
return super(RegistryDict, self).get(str(key), default)
return item['uid'], cls.init_tag(**item)
@classmethod
def init_tag(cls, tag_class, uid, alias=None, parameter=None):
'''
Initialise a new tag instance.
:param str tag_class: The tag class
:param str uid: The RFID UID
:param str alias: The alias
:param str parameter: The parameter
:return: The tag instance
:rtype: tags.Tag
'''
uid = str(uid).strip()
tag_class = getattr(tags, tag_class)
return tag_class(uid, alias, parameter)
def read(self):
'''
@ -63,7 +79,7 @@ class RegistryDict(dict):
with open(self.registry_path) as f:
data = json.load(f)
self.clear()
self.update(data)
self.update((self.unserialize_item(item) for item in data))
def write(self):
'''
@ -78,7 +94,35 @@ class RegistryDict(dict):
os.makedirs(directory)
with open(config, 'w') as f:
json.dump(self, f, indent=4)
json.dump([tag.dict for tag in self.values()], f, indent=4)
def register(self, tag_class, uid, alias=None, parameter=None):
'''
Register a new tag in the registry.
:param str tag_class: The tag class
:param str uid: The UID
:param str alias: The alias
:param str parameter: The parameter (optional)
:return: The tag
:rtype: tags.Tag
'''
LOGGER.info('Registering %s tag %s with parameter "%s"', tag_class, uid, parameter)
tag = self.init_tag(
tag_class=tag_class,
uid=uid,
alias=alias,
parameter=parameter
)
tag.validate()
self[uid] = tag
self.write()
return tag
REGISTRY = RegistryDict()

View file

@ -1,249 +0,0 @@
'''
Python module for Mopidy Pummeluff tags.
'''
__all__ = (
'Tag',
'TracklistTag',
'VolumeTag',
'PlayPauseTag',
'StopTag',
'ShutdownTag',
)
from logging import getLogger
from .registry import REGISTRY
from . import actions
LOGGER = getLogger(__name__)
class InvalidTagType(Exception):
'''
Exception which is thrown when an invalid tag type is defined.
'''
pass
class Tag:
'''
Base RFID tag class, which will implement the factory pattern in Python's
own :py:meth:`__new__` method.
'''
parameter_allowed = True
def __new__(cls, uid):
'''
Implement factory pattern and return correct tag instance.
'''
tag = REGISTRY.get(uid, {})
new_cls = cls.get_class(tag.get('type', ''))
if cls is Tag and cls is not new_cls:
instance = new_cls(uid=uid)
else:
instance = super().__new__(cls, uid=uid)
instance.registered = bool(tag)
instance.alias = tag.get('alias')
instance.parameter = tag.get('parameter')
return instance
def __init__(self, uid):
self.uid = uid
self.scanned = None
def __str__(self):
cls_name = self.__class__.__name__
identifier = self.alias or self.uid
return '<{}: {}>'.format(cls_name, identifier)
def __call__(self, core):
'''
Action method which is called when the tag is detected on the RFID
reader.
:param mopidy.core.Core core: The mopidy core instance
'''
args = [core]
if self.parameter:
args.append(self.parameter)
getattr(actions, self.action)(*args)
@staticmethod
def get_class(tag_type):
'''
Return class for specific tag type.
:param str tag_type: The tag type
:return: The tag class
:rtype: type
'''
try:
name = tag_type.title() + 'Tag'
cls = globals()[name]
assert issubclass(cls, Tag)
except (KeyError, AssertionError):
raise InvalidTagType('Tag class for type "{}" does\'t exist.'.format(tag_type))
return cls
@classmethod
def get_type(cls, tag_class=None):
'''
Return the type for a specific tag class.
:param type tag_class: The tag class
:return: The tag type
:rtype: str
'''
return (tag_class or cls).__name__[0:-3].lower()
@classmethod
def all(cls):
'''
Return all registered tags in a list.
:return: Registered tags
:rtype: list[Tag]
'''
return {uid: Tag(uid=uid) for uid in REGISTRY}
@classmethod
def register(cls, uid, alias=None, parameter=None, tag_type=None):
'''
Register tag in the registry.
:param str uid: The tag's UID
:param str alias: The tag's alias
:param str parameter: The optional parameter
:param str tag_type: The tag type
:return: The registered tag
:rtype: Tag
'''
if tag_type is None:
tag_type = cls.get_type(cls)
uid = uid.strip()
if not uid:
error = 'Invalid UID defined'
LOGGER.error(error)
raise ValueError(error)
LOGGER.info('Registering %s tag %s with parameter "%s"', tag_type, uid, parameter)
real_cls = cls.get_class(tag_type)
if real_cls == Tag:
error = 'Registering tags without explicit types are not allowed. ' \
'Set tag_type argument on Tag.register() ' \
'or use register() method of explicit tag classes.'
raise InvalidTagType(error)
if not real_cls.parameter_allowed and parameter:
raise ValueError('No parameter allowed for this tag')
elif hasattr(real_cls, 'validate_parameter'):
real_cls.validate_parameter(parameter)
REGISTRY[uid] = {
'type': tag_type,
'alias': alias.strip(),
'parameter': parameter.strip()
}
return Tag.all().get(uid)
@property
def dict(self):
'''
Return the dict version of this tag.
:return: The dict version of this tag
:rtype: dict
'''
tag_dict = {
'uid': self.uid,
'alias': self.alias,
'type': self.get_type(),
'parameter': self.parameter,
}
tag_dict['scanned'] = self.scanned
return tag_dict
@property
def action(self):
'''
Return a name of an action (function) defined in the
:py:mod:`mopidy_pummeluff.actions` Python module.
:return: An action name
:rtype: str
:raises NotImplementedError: When action property isn't defined
'''
cls = self.__class__.__name__
error = 'Missing action property in the %s class'
LOGGER.error(error, cls)
raise NotImplementedError(error % cls)
class TracklistTag(Tag):
'''
Replaces the current tracklist with the URI retreived from the tag's
parameter.
'''
action = 'replace_tracklist'
class VolumeTag(Tag):
'''
Sets the volume to the percentage value retreived from the tag's parameter.
'''
action = 'set_volume'
@staticmethod
def validate_parameter(parameter):
'''
Validates if the parameter is an integer between 0 and 100.
:param mixed parameter: The parameter
:raises ValueError: When parameter is invalid
'''
try:
number = int(parameter)
assert number >= 0 and number <= 100
except (ValueError, AssertionError):
raise ValueError('Volume parameter has to be a number between 0 and 100')
class PlayPauseTag(Tag):
'''
Pauses or resumes the playback, based on the current state.
'''
action = 'play_pause'
parameter_allowed = False
class StopTag(Tag):
'''
Stops the playback.
'''
action = 'stop'
parameter_allowed = False
class ShutdownTag(Tag):
'''
Shutting down the system.
'''
action = 'shutdown'
parameter_allowed = False

View file

@ -0,0 +1,17 @@
'''
Python module for Mopidy Pummeluff tags.
'''
__all__ = (
'Tracklist',
'Volume',
'PlayPause',
'Stop',
'Shutdown',
)
from .tracklist import Tracklist
from .volume import Volume
from .play_pause import PlayPause
from .stop import Stop
from .shutdown import Shutdown

View file

@ -0,0 +1,97 @@
'''
Python module for Mopidy Pummeluff base tag.
'''
__all__ = (
'Tag',
)
from logging import getLogger
LOGGER = getLogger(__name__)
class Tag:
'''
Base RFID tag class, which will implement the factory pattern in Python's
own :py:meth:`__new__` method.
'''
parameterised = True
def __init__(self, uid, alias=None, parameter=None):
'''
Concstructor.
'''
self.uid = uid
self.alias = alias
self.parameter = parameter
def __str__(self):
'''
String representation of tag.
:return: The alias
:rtype: str
'''
return self.alias or self.uid
def __repr__(self):
'''
Instance representation of tag.
:return: The class name and UID
:rtype: str
'''
identifier = self.alias or self.uid
return f'<{self.__class__.__name__} {identifier}>'
def __call__(self, core):
'''
Action method which is called when the tag is detected on the RFID
reader.
:param mopidy.core.Core core: The mopidy core instance
'''
args = [core]
if self.parameter:
args.append(self.parameter)
self.action(*args)
@property
def dict(self):
'''
Dict representation of the tag.
'''
return {
'tag_class': self.__class__.__name__,
'uid': self.uid,
'alias': self.alias or '',
'parameter': self.parameter or ''
}
@property
def action(self):
'''
Return a name of an action (function) defined in the
:py:mod:`mopidy_pummeluff.actions` Python module.
:return: An action name
:rtype: str
:raises NotImplementedError: When action property isn't defined
'''
cls = self.__class__.__name__
error = 'Missing action property in the %s class'
LOGGER.error(error, cls)
raise NotImplementedError(error % cls)
def validate(self):
'''
Validate parameter.
:raises ValueError: When parameter is not allowed but defined
'''
if self.parameterised and not self.parameter:
raise ValueError('Parameter required for this tag')
if not self.parameterised and self.parameter:
raise ValueError('No parameter allowed for this tag')

View file

@ -0,0 +1,18 @@
'''
Python module for Mopidy Pummeluff play pause tag.
'''
__all__ = (
'PlayPause',
)
from mopidy_pummeluff.actions import play_pause
from .base import Tag
class PlayPause(Tag):
'''
Pauses or resumes the playback, based on the current state.
'''
action = play_pause
parameterised = False

View file

@ -0,0 +1,18 @@
'''
Python module for Mopidy Pummeluff shutdown tag.
'''
__all__ = (
'Shutdown',
)
from mopidy_pummeluff.actions import shutdown
from .base import Tag
class Shutdown(Tag):
'''
Shutting down the system.
'''
action = shutdown
parameterised = False

View file

@ -0,0 +1,18 @@
'''
Python module for Mopidy Pummeluff stop tag.
'''
__all__ = (
'Stop',
)
from mopidy_pummeluff.actions import stop
from .base import Tag
class Stop(Tag):
'''
Stops the playback.
'''
action = stop
parameterised = False

View file

@ -0,0 +1,19 @@
'''
Python module for Mopidy Pummeluff tracklist tag.
'''
__all__ = (
'Tracklist',
)
from mopidy_pummeluff.actions import replace_tracklist
from .base import Tag
class Tracklist(Tag):
'''
Replaces the current tracklist with the URI retreived from the tag's
parameter.
'''
action = replace_tracklist

View file

@ -0,0 +1,34 @@
'''
Python module for Mopidy Pummeluff volume tag.
'''
__all__ = (
'Volume',
)
from mopidy_pummeluff.actions import set_volume
from .base import Tag
class Volume(Tag):
'''
Sets the volume to the percentage value retreived from the tag's parameter.
'''
action = set_volume
def validate(self):
'''
Validates if the parameter is an integer between 0 and 100.
:param mixed parameter: The parameter
:raises ValueError: When parameter is invalid
'''
super().validate()
try:
number = int(self.parameter)
assert number >= 0 and number <= 100
except (ValueError, AssertionError):
raise ValueError('Volume parameter has to be a number between 0 and 100')