forked from platypush/platypush
Fabio Manganiello
7bbae55e44
It's better for entity managers to be stored in their own public package, instead of cluttering too much the namespace of their parent package.
160 lines
6 KiB
Python
160 lines
6 KiB
Python
import inspect
|
|
import json
|
|
from abc import ABC, abstractmethod
|
|
from datetime import datetime
|
|
from typing import Any, Optional, Dict, Collection, Type
|
|
|
|
from platypush.config import Config
|
|
from platypush.entities._base import Entity, EntitySavedCallback
|
|
from platypush.utils import get_plugin_name_by_class, get_redis
|
|
|
|
_entity_registry_varname = '_platypush/plugin_entity_registry'
|
|
|
|
|
|
class EntityManager(ABC):
|
|
"""
|
|
Base mixin for all the integrations that support entities mapping.
|
|
|
|
The classes that implement the entity manager need to implement the
|
|
:meth:`.transform_entities` method, which will convert the supported
|
|
entities from whichever format the integration supports to a collection of
|
|
:class:`platypush.entities.Entity` objects.
|
|
|
|
The converted entities will then be passed to the
|
|
:class:`platypush.entities.EntitiesEngine` whenever
|
|
:meth:`.publish_entities` is called
|
|
|
|
The implemented classes should also implement the :meth:`.status` method.
|
|
This method should retrieve the current state of the entities and call
|
|
:meth:`.publish_entities`.
|
|
"""
|
|
|
|
def __new__(cls, *_, **__) -> 'EntityManager':
|
|
register_entity_manager(cls)
|
|
return super().__new__(cls)
|
|
|
|
@abstractmethod
|
|
def transform_entities(self, entities: Collection[Any]) -> Collection[Entity]:
|
|
"""
|
|
This method takes a list of entities in any (plugin-specific)
|
|
format and converts them into a standardized collection of
|
|
`Entity` objects. Since this method is called by
|
|
:meth:`.publish_entities` before entity updates are published,
|
|
you may usually want to extend it to pre-process the entities
|
|
managed by your extension into the standard format before they
|
|
are stored and published to all the consumers.
|
|
"""
|
|
assert all(isinstance(e, Entity) for e in entities), (
|
|
'Expected all the instances to be entities, got '
|
|
f'{[e.__class__.__name__ for e in entities]}'
|
|
)
|
|
return entities
|
|
|
|
@abstractmethod
|
|
def status(self, *_, **__):
|
|
"""
|
|
All derived classes should implement this method.
|
|
|
|
At the very least, this method should refresh the current state of the
|
|
integration's entities and call :meth:`.publish_entities`.
|
|
|
|
It should also return the current state of the entities as a list of
|
|
serialized entities, if possible.
|
|
"""
|
|
raise NotImplementedError(
|
|
'The `status` method has not been implemented in '
|
|
f'{self.__class__.__name__}'
|
|
)
|
|
|
|
def _normalize_entities(self, entities: Collection[Entity]) -> Collection[Entity]:
|
|
for entity in entities:
|
|
if entity.id and not entity.external_id:
|
|
# Entity IDs can only refer to the internal primary key
|
|
entity.external_id = entity.id
|
|
entity.id = None # type: ignore
|
|
|
|
entity.plugin = get_plugin_name_by_class(self.__class__) # type: ignore
|
|
entity.updated_at = datetime.utcnow() # type: ignore
|
|
entity.children = self._normalize_entities(entity.children)
|
|
|
|
return entities
|
|
|
|
def publish_entities(
|
|
self,
|
|
entities: Optional[Collection[Any]],
|
|
callback: Optional[EntitySavedCallback] = None,
|
|
) -> Collection[Entity]:
|
|
"""
|
|
Publishes a list of entities. The downstream consumers include:
|
|
|
|
- The entity persistence manager
|
|
- The web server
|
|
- Any consumer subscribed to
|
|
:class:`platypush.message.event.entities.EntityUpdateEvent`
|
|
events (e.g. web clients)
|
|
|
|
It also accepts an optional callback that will be called when each of
|
|
the entities in the set is flushed to the database.
|
|
|
|
You usually don't need to override this class (but you may want to
|
|
extend :meth:`.transform_entities` instead if your extension doesn't
|
|
natively handle `Entity` objects).
|
|
"""
|
|
from platypush.entities import publish_entities
|
|
|
|
transformed_entities = self._normalize_entities(
|
|
self.transform_entities(entities or [])
|
|
)
|
|
|
|
publish_entities(transformed_entities, callback=callback)
|
|
return transformed_entities
|
|
|
|
|
|
def register_entity_manager(cls: Type[EntityManager]):
|
|
"""
|
|
Associates a plugin as a manager for a certain entity type.
|
|
You usually don't have to call this method directly.
|
|
"""
|
|
entity_managers = [c for c in inspect.getmro(cls) if issubclass(c, EntityManager)]
|
|
|
|
plugin_name = get_plugin_name_by_class(cls) or ''
|
|
redis = get_redis()
|
|
registry = get_plugin_entity_registry()
|
|
registry_by_plugin = set(registry['by_plugin'].get(plugin_name, []))
|
|
|
|
for manager in entity_managers:
|
|
entity_type_name = manager.__name__
|
|
registry_by_type = set(registry['by_type'].get(entity_type_name, []))
|
|
registry_by_plugin.add(entity_type_name)
|
|
registry_by_type.add(plugin_name)
|
|
registry['by_plugin'][plugin_name] = list(registry_by_plugin)
|
|
registry['by_type'][entity_type_name] = list(registry_by_type)
|
|
|
|
redis.mset({_entity_registry_varname: json.dumps(registry)})
|
|
|
|
|
|
def get_plugin_entity_registry() -> Dict[str, Dict[str, Collection[str]]]:
|
|
"""
|
|
Get the `plugin->entity_types` and `entity_type->plugin`
|
|
mappings supported by the current configuration.
|
|
"""
|
|
redis = get_redis()
|
|
registry = redis.mget([_entity_registry_varname])[0]
|
|
try:
|
|
registry = json.loads((registry or b'').decode())
|
|
except (TypeError, ValueError):
|
|
return {'by_plugin': {}, 'by_type': {}}
|
|
|
|
enabled_plugins = set(Config.get_plugins().keys())
|
|
|
|
return {
|
|
'by_plugin': {
|
|
plugin_name: entity_types
|
|
for plugin_name, entity_types in registry.get('by_plugin', {}).items()
|
|
if plugin_name in enabled_plugins
|
|
},
|
|
'by_type': {
|
|
entity_type: [p for p in plugins if p in enabled_plugins]
|
|
for entity_type, plugins in registry.get('by_type', {}).items()
|
|
},
|
|
}
|