forked from platypush/platypush
386 lines
13 KiB
Python
386 lines
13 KiB
Python
from collections import defaultdict
|
|
import importlib
|
|
import inspect
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import pickle
|
|
import pkgutil
|
|
from types import ModuleType
|
|
from typing import Callable, Dict, Generator, Optional, Type, Union
|
|
|
|
from platypush.backend import Backend
|
|
from platypush.config import Config
|
|
from platypush.plugins import Plugin, action
|
|
from platypush.message.event import Event
|
|
from platypush.message.response import Response
|
|
from platypush.utils import (
|
|
get_backend_class_by_name,
|
|
get_backend_name_by_class,
|
|
get_plugin_class_by_name,
|
|
get_plugin_name_by_class,
|
|
)
|
|
from platypush.utils.manifest import Manifest, scan_manifests
|
|
|
|
from ._context import ComponentContext
|
|
from ._model import (
|
|
BackendModel,
|
|
EventModel,
|
|
Model,
|
|
PluginModel,
|
|
ResponseModel,
|
|
)
|
|
from ._serialize import ProcedureEncoder
|
|
|
|
|
|
class InspectPlugin(Plugin):
|
|
"""
|
|
This plugin can be used to inspect platypush plugins and backends
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self._components_cache_file = os.path.join(
|
|
Config.get('workdir'), # type: ignore
|
|
'components.cache', # type: ignore
|
|
)
|
|
self._components_context: Dict[type, ComponentContext] = defaultdict(
|
|
ComponentContext
|
|
)
|
|
self._components_cache: Dict[type, dict] = defaultdict(dict)
|
|
self._load_components_cache()
|
|
|
|
def _load_components_cache(self):
|
|
"""
|
|
Loads the components cache from disk.
|
|
"""
|
|
try:
|
|
with open(self._components_cache_file, 'rb') as f:
|
|
self._components_cache = pickle.load(f)
|
|
except OSError:
|
|
return
|
|
|
|
def _flush_components_cache(self):
|
|
"""
|
|
Flush the current components cache to disk.
|
|
"""
|
|
with open(self._components_cache_file, 'wb') as f:
|
|
pickle.dump(self._components_cache, f)
|
|
|
|
def _get_cached_component(
|
|
self, base_type: type, comp_type: type
|
|
) -> Optional[Model]:
|
|
"""
|
|
Retrieve a cached component's ``Model``.
|
|
|
|
:param base_type: The base type of the component (e.g. ``Plugin`` or
|
|
``Backend``).
|
|
:param comp_type: The specific type of the component (e.g.
|
|
``MusicMpdPlugin`` or ``HttpBackend``).
|
|
:return: The cached component's ``Model`` if it exists, otherwise null.
|
|
"""
|
|
return self._components_cache.get(base_type, {}).get(comp_type)
|
|
|
|
def _cache_component(
|
|
self,
|
|
base_type: type,
|
|
comp_type: type,
|
|
model: Model,
|
|
index_by_module: bool = False,
|
|
):
|
|
"""
|
|
Cache the ``Model`` object for a component.
|
|
|
|
:param base_type: The base type of the component (e.g. ``Plugin`` or
|
|
``Backend``).
|
|
:param comp_type: The specific type of the component (e.g.
|
|
``MusicMpdPlugin`` or ``HttpBackend``).
|
|
:param model: The ``Model`` object to cache.
|
|
:param index_by_module: If ``True``, the ``Model`` object will be
|
|
indexed according to the ``base_type -> module -> comp_type``
|
|
mapping, otherwise ``base_type -> comp_type``.
|
|
"""
|
|
if index_by_module:
|
|
if not self._components_cache.get(base_type, {}).get(model.package):
|
|
self._components_cache[base_type][model.package] = {}
|
|
self._components_cache[base_type][model.package][comp_type] = model
|
|
else:
|
|
self._components_cache[base_type][comp_type] = model
|
|
|
|
def _scan_integrations(self, base_type: type):
|
|
"""
|
|
A generator that scans the manifest files given a ``base_type``
|
|
(``Plugin`` or ``Backend``) and yields the parsed submodules.
|
|
"""
|
|
for mf_file in scan_manifests(base_type):
|
|
manifest = Manifest.from_file(mf_file)
|
|
try:
|
|
yield importlib.import_module(manifest.package)
|
|
except Exception as e:
|
|
self.logger.debug(
|
|
'Could not import module %s: %s',
|
|
manifest.package,
|
|
e,
|
|
)
|
|
continue
|
|
|
|
def _scan_modules(self, base_type: type) -> Generator[ModuleType, None, None]:
|
|
"""
|
|
A generator that scan the modules given a ``base_type`` (e.g. ``Event``).
|
|
|
|
Unlike :meth:`._scan_integrations`, this method recursively scans the
|
|
modules using ``pkgutil`` instead of using the information provided in
|
|
the integrations' manifest files.
|
|
"""
|
|
prefix = base_type.__module__ + '.'
|
|
path = str(pathlib.Path(inspect.getfile(base_type)).parent)
|
|
|
|
for _, modname, _ in pkgutil.walk_packages(
|
|
path=[path], prefix=prefix, onerror=lambda _: None
|
|
):
|
|
try:
|
|
yield importlib.import_module(modname)
|
|
except Exception as e:
|
|
self.logger.debug('Could not import module %s: %s', modname, e)
|
|
continue
|
|
|
|
def _init_component(
|
|
self,
|
|
base_type: type,
|
|
comp_type: type,
|
|
model_type: Type[Model],
|
|
index_by_module: bool = False,
|
|
) -> Model:
|
|
"""
|
|
Initialize a component's ``Model`` object and cache it.
|
|
|
|
:param base_type: The base type of the component (e.g. ``Plugin`` or
|
|
``Backend``).
|
|
:param comp_type: The specific type of the component (e.g.
|
|
``MusicMpdPlugin`` or ``HttpBackend``).
|
|
:param model_type: The type of the ``Model`` object that should be
|
|
created.
|
|
:param index_by_module: If ``True``, the ``Model`` object will be
|
|
indexed according to the ``base_type -> module -> comp_type``
|
|
mapping, otherwise ``base_type -> comp_type``.
|
|
:return: The initialized component's ``Model`` object.
|
|
"""
|
|
prefix = base_type.__module__ + '.'
|
|
comp_file = inspect.getsourcefile(comp_type)
|
|
model = None
|
|
mtime = None
|
|
|
|
if comp_file:
|
|
mtime = os.stat(comp_file).st_mtime
|
|
cached_model = self._get_cached_component(base_type, comp_type)
|
|
|
|
# Only update the component model if its source file was
|
|
# modified since the last time it was scanned
|
|
if (
|
|
cached_model
|
|
and cached_model.last_modified
|
|
and mtime <= cached_model.last_modified
|
|
):
|
|
model = cached_model
|
|
|
|
if not model:
|
|
self.logger.info('Scanning component %s', comp_type.__name__)
|
|
model = model_type(comp_type, prefix=prefix, last_modified=mtime)
|
|
|
|
self._cache_component(
|
|
base_type, comp_type, model, index_by_module=index_by_module
|
|
)
|
|
return model
|
|
|
|
def _init_modules(
|
|
self,
|
|
base_type: type,
|
|
model_type: Type[Model],
|
|
):
|
|
"""
|
|
Initializes, parses and caches all the components of a given type.
|
|
|
|
Unlike :meth:`._scan_integrations`, this method inspects all the
|
|
members of a ``module`` for those that match the given ``base_type``
|
|
instead of relying on the information provided in the manifest.
|
|
|
|
It is a bit more inefficient, but it works fine for simple components
|
|
(like entities and messages) that don't require extra recursive parsing
|
|
logic for their docs (unlike plugins).
|
|
"""
|
|
for module in self._scan_modules(base_type):
|
|
for _, obj_type in inspect.getmembers(module):
|
|
if (
|
|
inspect.isclass(obj_type)
|
|
and issubclass(obj_type, base_type)
|
|
# Exclude the base_type itself
|
|
and obj_type != base_type
|
|
):
|
|
self._init_component(
|
|
base_type=base_type,
|
|
comp_type=obj_type,
|
|
model_type=model_type,
|
|
index_by_module=True,
|
|
)
|
|
|
|
def _init_integrations(
|
|
self,
|
|
base_type: Type[Union[Plugin, Backend]],
|
|
model_type: Type[Union[PluginModel, BackendModel]],
|
|
class_by_name: Callable[[str], Optional[type]],
|
|
):
|
|
"""
|
|
Initializes, parses and caches all the integrations of a given type.
|
|
|
|
:param base_type: The base type of the component (e.g. ``Plugin`` or
|
|
``Backend``).
|
|
:param model_type: The type of the ``Model`` objects that should be
|
|
created.
|
|
:param class_by_name: A function that returns the class of a given
|
|
integration given its qualified name.
|
|
"""
|
|
for module in self._scan_integrations(base_type):
|
|
comp_name = '.'.join(module.__name__.split('.')[2:])
|
|
comp_type = class_by_name(comp_name)
|
|
if not comp_type:
|
|
continue
|
|
|
|
self._init_component(
|
|
base_type=base_type,
|
|
comp_type=comp_type,
|
|
model_type=model_type,
|
|
)
|
|
|
|
self._flush_components_cache()
|
|
|
|
def _init_plugins(self):
|
|
"""
|
|
Initializes and caches all the available plugins.
|
|
"""
|
|
self._init_integrations(
|
|
base_type=Plugin,
|
|
model_type=PluginModel,
|
|
class_by_name=get_plugin_class_by_name,
|
|
)
|
|
|
|
def _init_backends(self):
|
|
"""
|
|
Initializes and caches all the available backends.
|
|
"""
|
|
self._init_integrations(
|
|
base_type=Backend,
|
|
model_type=BackendModel,
|
|
class_by_name=get_backend_class_by_name,
|
|
)
|
|
|
|
def _init_events(self):
|
|
"""
|
|
Initializes and caches all the available events.
|
|
"""
|
|
self._init_modules(
|
|
base_type=Event,
|
|
model_type=EventModel,
|
|
)
|
|
|
|
def _init_responses(self):
|
|
"""
|
|
Initializes and caches all the available responses.
|
|
"""
|
|
self._init_modules(
|
|
base_type=Response,
|
|
model_type=ResponseModel,
|
|
)
|
|
|
|
def _init_components(self, base_type: type, initializer: Callable[[], None]):
|
|
"""
|
|
Context manager boilerplate for the other ``_init_*`` methods.
|
|
"""
|
|
ctx = self._components_context[base_type]
|
|
with ctx.init_lock:
|
|
if not ctx.refreshed.is_set():
|
|
initializer()
|
|
ctx.refreshed.set()
|
|
|
|
@action
|
|
def get_all_plugins(self):
|
|
"""
|
|
Get information about all the available plugins.
|
|
"""
|
|
self._init_components(Plugin, self._init_plugins)
|
|
return json.dumps(
|
|
{
|
|
get_plugin_name_by_class(cls): dict(plugin)
|
|
for cls, plugin in self._components_cache.get(Plugin, {}).items()
|
|
}
|
|
)
|
|
|
|
@action
|
|
def get_all_backends(self):
|
|
"""
|
|
Get information about all the available backends.
|
|
"""
|
|
self._init_components(Backend, self._init_backends)
|
|
return json.dumps(
|
|
{
|
|
get_backend_name_by_class(cls): dict(backend)
|
|
for cls, backend in self._components_cache.get(Backend, {}).items()
|
|
}
|
|
)
|
|
|
|
@action
|
|
def get_all_events(self):
|
|
"""
|
|
Get information about all the available events.
|
|
"""
|
|
self._init_components(Event, self._init_events)
|
|
return json.dumps(
|
|
{
|
|
package: {
|
|
obj_type.__name__: dict(event_model)
|
|
for obj_type, event_model in events.items()
|
|
}
|
|
for package, events in self._components_cache.get(Event, {}).items()
|
|
}
|
|
)
|
|
|
|
@action
|
|
def get_all_responses(self):
|
|
"""
|
|
Get information about all the available responses.
|
|
"""
|
|
self._init_components(Response, self._init_responses)
|
|
return json.dumps(
|
|
{
|
|
package: {
|
|
obj_type.__name__: dict(response_model)
|
|
for obj_type, response_model in responses.items()
|
|
}
|
|
for package, responses in self._components_cache.get(
|
|
Response, {}
|
|
).items()
|
|
}
|
|
)
|
|
|
|
@action
|
|
def get_procedures(self) -> dict:
|
|
"""
|
|
Get the list of procedures installed on the device.
|
|
"""
|
|
return json.loads(json.dumps(Config.get_procedures(), cls=ProcedureEncoder))
|
|
|
|
@action
|
|
def get_config(self, entry: Optional[str] = None) -> Optional[dict]:
|
|
"""
|
|
Return the configuration of the application or of a section.
|
|
|
|
:param entry: [Optional] configuration entry name to retrieve (e.g. ``workdir`` or ``backend.http``).
|
|
:return: The requested configuration object.
|
|
"""
|
|
if entry:
|
|
return Config.get(entry)
|
|
|
|
return Config.get()
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|