platypush/platypush/entities/managers/__init__.py
Fabio Manganiello 7bbae55e44
platypush.entities._managers -> platypush.entities.managers.
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.
2023-03-26 03:46:06 +02:00

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()
},
}