Refactor publication (#65)
* decoupled notifiers from event * stub * publishers working * fixed format CLI * fixed unit tests * renamed abstractnotifier * added another excluded character
This commit is contained in:
parent
6f81522ad0
commit
b6b2402767
|
@ -1,34 +0,0 @@
|
||||||
[default.publisher.telegram]
|
|
||||||
active=true
|
|
||||||
chat_id="xxx"
|
|
||||||
msg_template_path="xxx"
|
|
||||||
token="xxx"
|
|
||||||
username="xxx"
|
|
||||||
[default.publisher.facebook]
|
|
||||||
active=false
|
|
||||||
[default.publisher.zulip]
|
|
||||||
active=true
|
|
||||||
chat_id="xxx"
|
|
||||||
subject="xxx"
|
|
||||||
bot_token="xxx"
|
|
||||||
bot_email="xxx"
|
|
||||||
[default.publisher.twitter]
|
|
||||||
active=false
|
|
||||||
[default.publisher.mastodon]
|
|
||||||
active=false
|
|
||||||
|
|
||||||
[default.notifier.telegram]
|
|
||||||
active=true
|
|
||||||
chat_id="xxx"
|
|
||||||
token="xxx"
|
|
||||||
username="xxx"
|
|
||||||
[default.notifier.zulip]
|
|
||||||
active=true
|
|
||||||
chat_id="xxx"
|
|
||||||
subject="xxx"
|
|
||||||
bot_token="xxx"
|
|
||||||
bot_email="xxx"
|
|
||||||
[default.notifier.twitter]
|
|
||||||
active=false
|
|
||||||
[default.notifier.mastodon]
|
|
||||||
active=false
|
|
|
@ -2,10 +2,10 @@ import click
|
||||||
|
|
||||||
from mobilizon_reshare.event.event import MobilizonEvent
|
from mobilizon_reshare.event.event import MobilizonEvent
|
||||||
from mobilizon_reshare.models.event import Event
|
from mobilizon_reshare.models.event import Event
|
||||||
from mobilizon_reshare.publishers.coordinator import PublisherCoordinator
|
from mobilizon_reshare.publishers.platforms.platform_mapping import get_formatter_class
|
||||||
|
|
||||||
|
|
||||||
async def format_event(event_id, publisher):
|
async def format_event(event_id, publisher_name: str):
|
||||||
event = await Event.get_or_none(mobilizon_id=event_id).prefetch_related(
|
event = await Event.get_or_none(mobilizon_id=event_id).prefetch_related(
|
||||||
"publications__publisher"
|
"publications__publisher"
|
||||||
)
|
)
|
||||||
|
@ -13,5 +13,5 @@ async def format_event(event_id, publisher):
|
||||||
click.echo(f"Event with mobilizon_id {event_id} not found.")
|
click.echo(f"Event with mobilizon_id {event_id} not found.")
|
||||||
return
|
return
|
||||||
event = MobilizonEvent.from_model(event)
|
event = MobilizonEvent.from_model(event)
|
||||||
message = PublisherCoordinator.get_formatted_message(event, publisher)
|
message = get_formatter_class(publisher_name)().get_message_from_event(event)
|
||||||
click.echo(message)
|
click.echo(message)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import logging.config
|
import logging.config
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from mobilizon_reshare.event.event_selection_strategies import select_event_to_publish
|
from mobilizon_reshare.event.event_selection_strategies import select_event_to_publish
|
||||||
from mobilizon_reshare.mobilizon.events import get_unpublished_events
|
from mobilizon_reshare.mobilizon.events import get_unpublished_events
|
||||||
from mobilizon_reshare.models.publication import PublicationStatus
|
from mobilizon_reshare.models.publication import PublicationStatus
|
||||||
|
from mobilizon_reshare.publishers.abstract import EventPublication
|
||||||
from mobilizon_reshare.publishers.coordinator import (
|
from mobilizon_reshare.publishers.coordinator import (
|
||||||
PublicationFailureNotifiersCoordinator,
|
PublicationFailureNotifiersCoordinator,
|
||||||
)
|
)
|
||||||
|
@ -43,16 +45,24 @@ async def main():
|
||||||
)
|
)
|
||||||
|
|
||||||
if event:
|
if event:
|
||||||
|
|
||||||
waiting_publications = await publications_with_status(
|
|
||||||
status=PublicationStatus.WAITING,
|
|
||||||
event_mobilizon_id=event.mobilizon_id,
|
|
||||||
)
|
|
||||||
logger.debug(f"Event to publish found: {event.name}")
|
logger.debug(f"Event to publish found: {event.name}")
|
||||||
report = PublisherCoordinator(event, waiting_publications).run()
|
|
||||||
await save_publication_report(report, waiting_publications)
|
|
||||||
PublicationFailureNotifiersCoordinator(event, report).notify_failures()
|
|
||||||
|
|
||||||
return 0 if report.successful else 1
|
waiting_publications_models = await publications_with_status(
|
||||||
|
status=PublicationStatus.WAITING, event_mobilizon_id=event.mobilizon_id,
|
||||||
|
)
|
||||||
|
waiting_publications = list(
|
||||||
|
map(
|
||||||
|
partial(EventPublication.from_orm, event=event),
|
||||||
|
waiting_publications_models.values(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
reports = PublisherCoordinator(waiting_publications).run()
|
||||||
|
|
||||||
|
await save_publication_report(reports, waiting_publications_models)
|
||||||
|
for _, report in reports.reports.items():
|
||||||
|
PublicationFailureNotifiersCoordinator(report).notify_failure()
|
||||||
|
|
||||||
|
return 0 if reports.successful else 1
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from dynaconf.utils.boxing import DynaBox
|
from dynaconf.utils.boxing import DynaBox
|
||||||
from jinja2 import Environment, FileSystemLoader, Template
|
from jinja2 import Environment, FileSystemLoader, Template
|
||||||
from requests import Response
|
|
||||||
|
|
||||||
from mobilizon_reshare.config.config import get_settings
|
from mobilizon_reshare.config.config import get_settings
|
||||||
from mobilizon_reshare.event.event import MobilizonEvent
|
from mobilizon_reshare.event.event import MobilizonEvent
|
||||||
|
from mobilizon_reshare.models.publication import Publication as PublicationModel
|
||||||
from .exceptions import PublisherError, InvalidAttribute
|
from .exceptions import PublisherError, InvalidAttribute
|
||||||
|
|
||||||
JINJA_ENV = Environment(loader=FileSystemLoader("/"))
|
JINJA_ENV = Environment(loader=FileSystemLoader("/"))
|
||||||
|
@ -15,52 +17,7 @@ JINJA_ENV = Environment(loader=FileSystemLoader("/"))
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AbstractNotifier(ABC):
|
class LoggerMixin:
|
||||||
"""
|
|
||||||
Generic notifier class.
|
|
||||||
Shall be inherited from specific subclasses that will manage validation
|
|
||||||
process for messages and credentials, text formatting, posting, etc.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
- ``message``: a formatted ``str``
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Non-abstract subclasses should define ``_conf`` as a 2-tuple, where the
|
|
||||||
# first element is the type of class (either 'notifier' or 'publisher') and
|
|
||||||
# the second the name of its service (ie: 'facebook', 'telegram')
|
|
||||||
_conf = tuple()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return type(self).__name__
|
|
||||||
|
|
||||||
__str__ = __repr__
|
|
||||||
|
|
||||||
@property
|
|
||||||
def conf(self) -> DynaBox:
|
|
||||||
"""
|
|
||||||
Retrieves class's settings.
|
|
||||||
"""
|
|
||||||
cls = type(self)
|
|
||||||
if cls in (AbstractPublisher, AbstractNotifier):
|
|
||||||
raise InvalidAttribute(
|
|
||||||
"Abstract classes cannot access notifiers/publishers' settings"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
t, n = cls._conf or tuple()
|
|
||||||
return get_settings()[t][n]
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
raise InvalidAttribute(
|
|
||||||
f"Class {cls.__name__} has invalid ``_conf`` attribute"
|
|
||||||
f" (should be 2-tuple)"
|
|
||||||
)
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def send(self, message):
|
|
||||||
"""
|
|
||||||
Sends a message to the target channel
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _log_debug(self, msg, *args, **kwargs):
|
def _log_debug(self, msg, *args, **kwargs):
|
||||||
self.__log(logging.DEBUG, msg, *args, **kwargs)
|
self.__log(logging.DEBUG, msg, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -82,6 +39,65 @@ class AbstractNotifier(ABC):
|
||||||
if raise_error is not None:
|
if raise_error is not None:
|
||||||
raise raise_error(msg)
|
raise raise_error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfLoaderMixin:
|
||||||
|
_conf = tuple()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conf(self) -> DynaBox:
|
||||||
|
"""
|
||||||
|
Retrieves class's settings.
|
||||||
|
"""
|
||||||
|
cls = type(self)
|
||||||
|
|
||||||
|
try:
|
||||||
|
t, n = cls._conf or tuple()
|
||||||
|
return get_settings()[t][n]
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
raise InvalidAttribute(
|
||||||
|
f"Class {cls.__name__} has invalid ``_conf`` attribute"
|
||||||
|
f" (should be 2-tuple)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractPlatform(ABC, LoggerMixin, ConfLoaderMixin):
|
||||||
|
"""
|
||||||
|
Generic notifier class.
|
||||||
|
Shall be inherited from specific subclasses that will manage validation
|
||||||
|
process for messages and credentials, text formatting, posting, etc.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
- ``message``: a formatted ``str``
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Non-abstract subclasses should define ``_conf`` as a 2-tuple, where the
|
||||||
|
# first element is the type of class (either 'notifier' or 'publisher') and
|
||||||
|
# the second the name of its service (ie: 'facebook', 'telegram')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return type(self).__name__
|
||||||
|
|
||||||
|
__str__ = __repr__
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _send(self, message: str):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def send(self, message: str):
|
||||||
|
"""
|
||||||
|
Sends a message to the target channel
|
||||||
|
"""
|
||||||
|
message = self._preprocess_message(message)
|
||||||
|
response = self._send(message)
|
||||||
|
self._validate_response(response)
|
||||||
|
|
||||||
|
def _preprocess_message(self, message: str):
|
||||||
|
return message
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _validate_response(self, response):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def are_credentials_valid(self) -> bool:
|
def are_credentials_valid(self) -> bool:
|
||||||
try:
|
try:
|
||||||
self.validate_credentials()
|
self.validate_credentials()
|
||||||
|
@ -98,59 +114,10 @@ class AbstractNotifier(ABC):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractEventFormatter(LoggerMixin, ConfLoaderMixin):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def publish(self) -> None:
|
def validate_event(self, message) -> None:
|
||||||
"""
|
|
||||||
Publishes the actual post on social media.
|
|
||||||
Should raise ``PublisherError`` (or one of its subclasses) if
|
|
||||||
anything goes wrong.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def is_message_valid(self) -> bool:
|
|
||||||
try:
|
|
||||||
self.validate_message()
|
|
||||||
except PublisherError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def validate_message(self) -> None:
|
|
||||||
"""
|
|
||||||
Validates notifier's message.
|
|
||||||
Should raise ``PublisherError`` (or one of its subclasses) if message
|
|
||||||
is not valid.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractPublisher(AbstractNotifier):
|
|
||||||
"""
|
|
||||||
Generic publisher class.
|
|
||||||
Shall be inherited from specific subclasses that will manage validation
|
|
||||||
process for events and credentials, text formatting, posting, etc.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
- ``event``: a ``MobilizonEvent`` containing every useful info from
|
|
||||||
the event
|
|
||||||
- ``message``: a formatted ``str``
|
|
||||||
"""
|
|
||||||
|
|
||||||
_conf = tuple()
|
|
||||||
|
|
||||||
def __init__(self, event: MobilizonEvent):
|
|
||||||
self.event = event
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def is_event_valid(self) -> bool:
|
|
||||||
try:
|
|
||||||
self.validate_event()
|
|
||||||
except PublisherError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def validate_event(self) -> None:
|
|
||||||
"""
|
"""
|
||||||
Validates publisher's event.
|
Validates publisher's event.
|
||||||
Should raise ``PublisherError`` (or one of its subclasses) if event
|
Should raise ``PublisherError`` (or one of its subclasses) if event
|
||||||
|
@ -158,18 +125,18 @@ class AbstractPublisher(AbstractNotifier):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _preprocess_event(self):
|
def _preprocess_event(self, event):
|
||||||
"""
|
"""
|
||||||
Allows publishers to preprocess events before feeding them to the template
|
Allows publishers to preprocess events before feeding them to the template
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_message_from_event(self) -> str:
|
def get_message_from_event(self, event) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieves a message from the event itself.
|
Retrieves a message from the event itself.
|
||||||
"""
|
"""
|
||||||
self._preprocess_event()
|
event = self._preprocess_event(event)
|
||||||
return self.event.format(self.get_message_template())
|
return event.format(self.get_message_template())
|
||||||
|
|
||||||
def get_message_template(self) -> Template:
|
def get_message_template(self) -> Template:
|
||||||
"""
|
"""
|
||||||
|
@ -178,17 +145,45 @@ class AbstractPublisher(AbstractNotifier):
|
||||||
template_path = self.conf.msg_template_path or self.default_template_path
|
template_path = self.conf.msg_template_path or self.default_template_path
|
||||||
return JINJA_ENV.get_template(template_path)
|
return JINJA_ENV.get_template(template_path)
|
||||||
|
|
||||||
@abstractmethod
|
def is_message_valid(self, event: MobilizonEvent) -> bool:
|
||||||
def _send(self, message) -> Response:
|
try:
|
||||||
pass
|
self.validate_message(self.get_message_from_event(event))
|
||||||
|
except PublisherError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _validate_response(self, response: Response) -> None:
|
def validate_message(self, message: str) -> None:
|
||||||
pass
|
"""
|
||||||
|
Validates notifier's message.
|
||||||
|
Should raise ``PublisherError`` (or one of its subclasses) if message
|
||||||
|
is not valid.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def send(self, message):
|
def is_event_valid(self, event) -> bool:
|
||||||
res = self._send(message)
|
try:
|
||||||
self._validate_response(res)
|
self.validate_event(event)
|
||||||
|
except PublisherError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def publish(self) -> None:
|
|
||||||
self.send(message=self.get_message_from_event())
|
@dataclass
|
||||||
|
class EventPublication:
|
||||||
|
event: MobilizonEvent
|
||||||
|
id: UUID
|
||||||
|
publisher: AbstractPlatform
|
||||||
|
formatter: AbstractEventFormatter
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_orm(cls, model: PublicationModel, event: MobilizonEvent):
|
||||||
|
# imported here to avoid circular dependencies
|
||||||
|
from mobilizon_reshare.publishers.platforms.platform_mapping import (
|
||||||
|
get_publisher_class,
|
||||||
|
get_formatter_class,
|
||||||
|
)
|
||||||
|
|
||||||
|
publisher = get_publisher_class(model.publisher.name)()
|
||||||
|
formatter = get_formatter_class(model.publisher.name)()
|
||||||
|
return cls(event, model.id, publisher, formatter)
|
||||||
|
|
|
@ -1,32 +1,16 @@
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from mobilizon_reshare.event.event import MobilizonEvent
|
|
||||||
from mobilizon_reshare.models.publication import Publication
|
|
||||||
from mobilizon_reshare.models.publication import PublicationStatus
|
from mobilizon_reshare.models.publication import PublicationStatus
|
||||||
from mobilizon_reshare.publishers import get_active_notifiers, get_active_publishers
|
from mobilizon_reshare.publishers import get_active_notifiers
|
||||||
from mobilizon_reshare.publishers.abstract import AbstractPublisher
|
from mobilizon_reshare.publishers.abstract import EventPublication
|
||||||
from mobilizon_reshare.publishers.exceptions import PublisherError
|
from mobilizon_reshare.publishers.exceptions import PublisherError
|
||||||
from mobilizon_reshare.publishers.telegram import TelegramPublisher
|
from mobilizon_reshare.publishers.platforms.platform_mapping import get_notifier_class
|
||||||
from mobilizon_reshare.publishers.zulip import ZulipPublisher
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
name_to_publisher_class = {"telegram": TelegramPublisher, "zulip": ZulipPublisher}
|
|
||||||
|
|
||||||
|
|
||||||
class BuildPublisherMixin:
|
|
||||||
@staticmethod
|
|
||||||
def build_publishers(
|
|
||||||
event: MobilizonEvent, publisher_names
|
|
||||||
) -> dict[str, AbstractPublisher]:
|
|
||||||
|
|
||||||
return {
|
|
||||||
publisher_name: name_to_publisher_class[publisher_name](event)
|
|
||||||
for publisher_name in publisher_names
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PublicationReport:
|
class PublicationReport:
|
||||||
|
@ -37,7 +21,7 @@ class PublicationReport:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PublisherCoordinatorReport:
|
class PublisherCoordinatorReport:
|
||||||
publishers: dict[UUID, AbstractPublisher]
|
publications: List[EventPublication]
|
||||||
reports: dict[UUID, PublicationReport] = field(default_factory={})
|
reports: dict[UUID, PublicationReport] = field(default_factory={})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -47,118 +31,107 @@ class PublisherCoordinatorReport:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PublisherCoordinator(BuildPublisherMixin):
|
class PublisherCoordinator:
|
||||||
def __init__(self, event: MobilizonEvent, publications: dict[UUID, Publication]):
|
def __init__(self, publications: List[EventPublication]):
|
||||||
publishers = self.build_publishers(event, get_active_publishers())
|
self.publications = publications
|
||||||
self.publishers_by_publication_id = {
|
|
||||||
publication_id: publishers[publication.publisher.name]
|
|
||||||
for publication_id, publication in publications.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def run(self) -> PublisherCoordinatorReport:
|
def run(self) -> PublisherCoordinatorReport:
|
||||||
errors = self._validate()
|
errors = self._validate()
|
||||||
if errors:
|
if errors:
|
||||||
return PublisherCoordinatorReport(
|
return PublisherCoordinatorReport(
|
||||||
reports=errors, publishers=self.publishers_by_publication_id
|
reports=errors, publications=self.publications
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._post()
|
return self._post()
|
||||||
|
|
||||||
def _make_successful_report(self, failed_ids):
|
def _make_successful_report(self, failed_ids):
|
||||||
return {
|
return {
|
||||||
publication_id: PublicationReport(
|
publication.id: PublicationReport(
|
||||||
status=PublicationStatus.COMPLETED,
|
status=PublicationStatus.COMPLETED,
|
||||||
reason="",
|
reason="",
|
||||||
publication_id=publication_id,
|
publication_id=publication.id,
|
||||||
)
|
)
|
||||||
for publication_id in self.publishers_by_publication_id
|
for publication in self.publications
|
||||||
if publication_id not in failed_ids
|
if publication.id not in failed_ids
|
||||||
}
|
}
|
||||||
|
|
||||||
def _post(self):
|
def _post(self):
|
||||||
failed_publishers_reports = {}
|
failed_publishers_reports = {}
|
||||||
for publication_id, p in self.publishers_by_publication_id.items():
|
|
||||||
|
for publication in self.publications:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p.publish()
|
message = publication.formatter.get_message_from_event(
|
||||||
|
publication.event
|
||||||
|
)
|
||||||
|
publication.publisher.send(message)
|
||||||
except PublisherError as e:
|
except PublisherError as e:
|
||||||
failed_publishers_reports[publication_id] = PublicationReport(
|
failed_publishers_reports[publication.id] = PublicationReport(
|
||||||
status=PublicationStatus.FAILED,
|
status=PublicationStatus.FAILED,
|
||||||
reason=str(e),
|
reason=str(e),
|
||||||
publication_id=publication_id,
|
publication_id=publication.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
reports = failed_publishers_reports | self._make_successful_report(
|
reports = failed_publishers_reports | self._make_successful_report(
|
||||||
failed_publishers_reports.keys()
|
failed_publishers_reports.keys()
|
||||||
)
|
)
|
||||||
return PublisherCoordinatorReport(
|
return PublisherCoordinatorReport(
|
||||||
publishers=self.publishers_by_publication_id, reports=reports
|
publications=self.publications, reports=reports
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
errors: dict[UUID, PublicationReport] = {}
|
errors: dict[UUID, PublicationReport] = {}
|
||||||
for publication_id, p in self.publishers_by_publication_id.items():
|
|
||||||
|
for publication in self.publications:
|
||||||
|
|
||||||
reason = []
|
reason = []
|
||||||
if not p.are_credentials_valid():
|
if not publication.publisher.are_credentials_valid():
|
||||||
reason.append("Invalid credentials")
|
reason.append("Invalid credentials")
|
||||||
if not p.is_event_valid():
|
if not publication.formatter.is_event_valid(publication.event):
|
||||||
reason.append("Invalid event")
|
reason.append("Invalid event")
|
||||||
if not p.is_message_valid():
|
if not publication.formatter.is_message_valid(publication.event):
|
||||||
reason.append("Invalid message")
|
reason.append("Invalid message")
|
||||||
|
|
||||||
if len(reason) > 0:
|
if len(reason) > 0:
|
||||||
errors[publication_id] = PublicationReport(
|
errors[publication.id] = PublicationReport(
|
||||||
status=PublicationStatus.FAILED,
|
status=PublicationStatus.FAILED,
|
||||||
reason=", ".join(reason),
|
reason=", ".join(reason),
|
||||||
publication_id=publication_id,
|
publication_id=publication.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_formatted_message(event: MobilizonEvent, publisher: str) -> str:
|
|
||||||
"""
|
|
||||||
Returns the formatted message for a given event and publisher.
|
|
||||||
"""
|
|
||||||
if publisher not in name_to_publisher_class:
|
|
||||||
raise ValueError(
|
|
||||||
f"Publisher {publisher} does not exist.\nSupported publishers: "
|
|
||||||
f"{', '.join(list(name_to_publisher_class.keys()))}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return name_to_publisher_class[publisher](event).get_message_from_event()
|
class AbstractNotifiersCoordinator:
|
||||||
|
def __init__(self, message: str, notifiers=None):
|
||||||
|
self.message = message
|
||||||
|
self.notifiers = notifiers or [
|
||||||
|
get_notifier_class(notifier)() for notifier in get_active_notifiers()
|
||||||
|
]
|
||||||
|
|
||||||
|
def send_to_all(self):
|
||||||
class AbstractNotifiersCoordinator(BuildPublisherMixin):
|
|
||||||
def __init__(self, event: MobilizonEvent):
|
|
||||||
self.event = event
|
|
||||||
self.notifiers = self.build_publishers(event, get_active_notifiers())
|
|
||||||
|
|
||||||
def send_to_all(self, message):
|
|
||||||
# TODO: failure to notify should fail safely and write to a dedicated log
|
# TODO: failure to notify should fail safely and write to a dedicated log
|
||||||
for notifier in self.notifiers.values():
|
for notifier in self.notifiers:
|
||||||
notifier.send(message)
|
notifier.send(self.message)
|
||||||
|
|
||||||
|
|
||||||
class PublicationFailureNotifiersCoordinator(AbstractNotifiersCoordinator):
|
class PublicationFailureNotifiersCoordinator(AbstractNotifiersCoordinator):
|
||||||
def __init__(
|
def __init__(self, report: PublicationReport, notifiers=None):
|
||||||
self,
|
self.report = report
|
||||||
event: MobilizonEvent,
|
super(PublicationFailureNotifiersCoordinator, self).__init__(
|
||||||
publisher_coordinator_report: PublisherCoordinatorReport,
|
message=self.build_failure_message(), notifiers=notifiers
|
||||||
):
|
)
|
||||||
self.report = publisher_coordinator_report
|
|
||||||
super(PublicationFailureNotifiersCoordinator, self).__init__(event)
|
|
||||||
|
|
||||||
def build_failure_message(self, report: PublicationReport):
|
def build_failure_message(self):
|
||||||
|
report = self.report
|
||||||
return (
|
return (
|
||||||
f"Publication {report.publication_id} failed with status: {report.status}.\n"
|
f"Publication {report.publication_id} failed with status: {report.status}.\n"
|
||||||
f"Reason: {report.reason}"
|
f"Reason: {report.reason}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def notify_failures(self):
|
def notify_failure(self):
|
||||||
for publication_id, report in self.report.reports.items():
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Sending failure notifications for publication: {publication_id}"
|
f"Sending failure notifications for publication: {self.report.publication_id}"
|
||||||
)
|
)
|
||||||
if report.status == PublicationStatus.FAILED:
|
if self.report.status == PublicationStatus.FAILED:
|
||||||
self.send_to_all(self.build_failure_message(report))
|
self.send_to_all()
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
from mobilizon_reshare.publishers.platforms.telegram import (
|
||||||
|
TelegramPublisher,
|
||||||
|
TelegramFormatter,
|
||||||
|
TelegramNotifier,
|
||||||
|
)
|
||||||
|
from mobilizon_reshare.publishers.platforms.zulip import (
|
||||||
|
ZulipPublisher,
|
||||||
|
ZulipFormatter,
|
||||||
|
ZulipNotifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
name_to_publisher_class = {
|
||||||
|
"telegram": TelegramPublisher,
|
||||||
|
"zulip": ZulipPublisher,
|
||||||
|
}
|
||||||
|
name_to_formatter_class = {
|
||||||
|
"telegram": TelegramFormatter,
|
||||||
|
"zulip": ZulipFormatter,
|
||||||
|
}
|
||||||
|
name_to_notifier_class = {
|
||||||
|
"telegram": TelegramNotifier,
|
||||||
|
"zulip": ZulipNotifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_notifier_class(platform):
|
||||||
|
return name_to_notifier_class[platform]
|
||||||
|
|
||||||
|
|
||||||
|
def get_publisher_class(platform):
|
||||||
|
return name_to_publisher_class[platform]
|
||||||
|
|
||||||
|
|
||||||
|
def get_formatter_class(platform):
|
||||||
|
return name_to_formatter_class[platform]
|
|
@ -2,8 +2,12 @@ import pkg_resources
|
||||||
import requests
|
import requests
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
|
||||||
|
from mobilizon_reshare.event.event import MobilizonEvent
|
||||||
from mobilizon_reshare.formatting.description import html_to_markdown
|
from mobilizon_reshare.formatting.description import html_to_markdown
|
||||||
from mobilizon_reshare.publishers.abstract import AbstractPublisher
|
from mobilizon_reshare.publishers.abstract import (
|
||||||
|
AbstractEventFormatter,
|
||||||
|
AbstractPlatform,
|
||||||
|
)
|
||||||
from mobilizon_reshare.publishers.exceptions import (
|
from mobilizon_reshare.publishers.exceptions import (
|
||||||
InvalidBot,
|
InvalidBot,
|
||||||
InvalidCredentials,
|
InvalidCredentials,
|
||||||
|
@ -12,35 +16,50 @@ from mobilizon_reshare.publishers.exceptions import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TelegramPublisher(AbstractPublisher):
|
class TelegramFormatter(AbstractEventFormatter):
|
||||||
"""
|
|
||||||
Telegram publisher class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_conf = ("publisher", "telegram")
|
|
||||||
default_template_path = pkg_resources.resource_filename(
|
default_template_path = pkg_resources.resource_filename(
|
||||||
"mobilizon_reshare.publishers.templates", "telegram.tmpl.j2"
|
"mobilizon_reshare.publishers.templates", "telegram.tmpl.j2"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _escape_message(self, message: str) -> str:
|
_conf = ("publisher", "telegram")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def escape_message(message: str) -> str:
|
||||||
message = (
|
message = (
|
||||||
message.replace("-", "\\-")
|
message.replace("-", "\\-")
|
||||||
.replace(".", "\\.")
|
.replace(".", "\\.")
|
||||||
.replace("(", "\\(")
|
.replace("(", "\\(")
|
||||||
|
.replace("!", "\\!")
|
||||||
.replace(")", "\\)")
|
.replace(")", "\\)")
|
||||||
.replace("#", "")
|
.replace("#", "")
|
||||||
)
|
)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def _send(self, message: str) -> Response:
|
def validate_event(self, event: MobilizonEvent) -> None:
|
||||||
return requests.post(
|
text = event.description
|
||||||
url=f"https://api.telegram.org/bot{self.conf.token}/sendMessage",
|
if not (text and text.strip()):
|
||||||
json={
|
self._log_error("No text was found", raise_error=InvalidEvent)
|
||||||
"chat_id": self.conf.chat_id,
|
|
||||||
"text": self._escape_message(message),
|
def get_message_from_event(self, event: MobilizonEvent) -> str:
|
||||||
"parse_mode": "markdownv2",
|
return super(TelegramFormatter, self).get_message_from_event(event)
|
||||||
},
|
|
||||||
)
|
def validate_message(self, message: str) -> None:
|
||||||
|
# TODO implement
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _preprocess_event(self, event: MobilizonEvent):
|
||||||
|
event.description = html_to_markdown(event.description)
|
||||||
|
event.name = html_to_markdown(event.name)
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramPlatform(AbstractPlatform):
|
||||||
|
"""
|
||||||
|
Telegram publisher class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _preprocess_message(self, message: str):
|
||||||
|
return TelegramFormatter.escape_message(message)
|
||||||
|
|
||||||
def validate_credentials(self):
|
def validate_credentials(self):
|
||||||
conf = self.conf
|
conf = self.conf
|
||||||
|
@ -67,13 +86,19 @@ class TelegramPublisher(AbstractPublisher):
|
||||||
"Found a different bot than the expected one", raise_error=InvalidBot,
|
"Found a different bot than the expected one", raise_error=InvalidBot,
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_event(self) -> None:
|
def _send(self, message) -> Response:
|
||||||
text = self.event.description
|
return requests.post(
|
||||||
if not (text and text.strip()):
|
url=f"https://api.telegram.org/bot{self.conf.token}/sendMessage",
|
||||||
self._log_error("No text was found", raise_error=InvalidEvent)
|
json={
|
||||||
|
"chat_id": self.conf.chat_id,
|
||||||
|
"text": message,
|
||||||
|
"parse_mode": "markdownv2",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def _validate_response(self, res):
|
def _validate_response(self, res):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
self._log_error(
|
self._log_error(
|
||||||
|
@ -95,10 +120,12 @@ class TelegramPublisher(AbstractPublisher):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate_message(self) -> None:
|
|
||||||
# TODO implement
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _preprocess_event(self):
|
class TelegramPublisher(TelegramPlatform):
|
||||||
self.event.description = html_to_markdown(self.event.description)
|
|
||||||
self.event.name = html_to_markdown(self.event.name)
|
_conf = ("publisher", "telegram")
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramNotifier(TelegramPlatform):
|
||||||
|
|
||||||
|
_conf = ("notifier", "telegram")
|
|
@ -3,8 +3,12 @@ import requests
|
||||||
from requests import Response
|
from requests import Response
|
||||||
from requests.auth import HTTPBasicAuth
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
|
from mobilizon_reshare.event.event import MobilizonEvent
|
||||||
from mobilizon_reshare.formatting.description import html_to_markdown
|
from mobilizon_reshare.formatting.description import html_to_markdown
|
||||||
from mobilizon_reshare.publishers.abstract import AbstractPublisher
|
from mobilizon_reshare.publishers.abstract import (
|
||||||
|
AbstractPlatform,
|
||||||
|
AbstractEventFormatter,
|
||||||
|
)
|
||||||
from mobilizon_reshare.publishers.exceptions import (
|
from mobilizon_reshare.publishers.exceptions import (
|
||||||
InvalidBot,
|
InvalidBot,
|
||||||
InvalidCredentials,
|
InvalidCredentials,
|
||||||
|
@ -14,7 +18,28 @@ from mobilizon_reshare.publishers.exceptions import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ZulipPublisher(AbstractPublisher):
|
class ZulipFormatter(AbstractEventFormatter):
|
||||||
|
|
||||||
|
_conf = ("publisher", "zulip")
|
||||||
|
default_template_path = pkg_resources.resource_filename(
|
||||||
|
"mobilizon_reshare.publishers.templates", "zulip.tmpl.j2"
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_event(self, event: MobilizonEvent) -> None:
|
||||||
|
text = event.description
|
||||||
|
if not (text and text.strip()):
|
||||||
|
self._log_error("No text was found", raise_error=InvalidEvent)
|
||||||
|
|
||||||
|
def validate_message(self, message) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _preprocess_event(self, event: MobilizonEvent):
|
||||||
|
event.description = html_to_markdown(event.description)
|
||||||
|
event.name = html_to_markdown(event.name)
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
class ZulipPlatform(AbstractPlatform):
|
||||||
"""
|
"""
|
||||||
Zulip publisher class.
|
Zulip publisher class.
|
||||||
"""
|
"""
|
||||||
|
@ -32,11 +57,7 @@ class ZulipPublisher(AbstractPublisher):
|
||||||
return requests.post(
|
return requests.post(
|
||||||
url=self.api_uri + "messages",
|
url=self.api_uri + "messages",
|
||||||
auth=HTTPBasicAuth(self.conf.bot_email, self.conf.bot_token),
|
auth=HTTPBasicAuth(self.conf.bot_email, self.conf.bot_token),
|
||||||
data={
|
data={"type": "private", "to": f"[{self.user_id}]", "content": message},
|
||||||
"type": "private",
|
|
||||||
"to": f"[{self.user_id}]",
|
|
||||||
"content": message,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _send(self, message: str) -> Response:
|
def _send(self, message: str) -> Response:
|
||||||
|
@ -68,8 +89,7 @@ class ZulipPublisher(AbstractPublisher):
|
||||||
err.append("bot email")
|
err.append("bot email")
|
||||||
if err:
|
if err:
|
||||||
self._log_error(
|
self._log_error(
|
||||||
", ".join(err) + " is/are missing",
|
", ".join(err) + " is/are missing", raise_error=InvalidCredentials,
|
||||||
raise_error=InvalidCredentials,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
res = requests.get(
|
res = requests.get(
|
||||||
|
@ -80,8 +100,7 @@ class ZulipPublisher(AbstractPublisher):
|
||||||
|
|
||||||
if not data["is_bot"]:
|
if not data["is_bot"]:
|
||||||
self._log_error(
|
self._log_error(
|
||||||
"These user is not a bot",
|
"These user is not a bot", raise_error=InvalidBot,
|
||||||
raise_error=InvalidBot,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not bot_email == data["email"]:
|
if not bot_email == data["email"]:
|
||||||
|
@ -92,13 +111,7 @@ class ZulipPublisher(AbstractPublisher):
|
||||||
raise_error=InvalidBot,
|
raise_error=InvalidBot,
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_event(self) -> None:
|
|
||||||
text = self.event.description
|
|
||||||
if not (text and text.strip()):
|
|
||||||
self._log_error("No text was found", raise_error=InvalidEvent)
|
|
||||||
|
|
||||||
def validate_message(self) -> None:
|
def validate_message(self) -> None:
|
||||||
# We don't need this for Zulip.
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _validate_response(self, res) -> dict:
|
def _validate_response(self, res) -> dict:
|
||||||
|
@ -113,12 +126,17 @@ class ZulipPublisher(AbstractPublisher):
|
||||||
|
|
||||||
if data["result"] == "error":
|
if data["result"] == "error":
|
||||||
self._log_error(
|
self._log_error(
|
||||||
f"{res.status_code} Error - {data['msg']}",
|
f"{res.status_code} Error - {data['msg']}", raise_error=ZulipError,
|
||||||
raise_error=ZulipError,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _preprocess_event(self):
|
|
||||||
self.event.description = html_to_markdown(self.event.description)
|
class ZulipPublisher(ZulipPlatform):
|
||||||
self.event.name = html_to_markdown(self.event.name)
|
|
||||||
|
_conf = ("publisher", "zulip")
|
||||||
|
|
||||||
|
|
||||||
|
class ZulipNotifier(ZulipPlatform):
|
||||||
|
|
||||||
|
_conf = ("notifier", "zulip")
|
|
@ -5,13 +5,15 @@ import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from mobilizon_reshare.event.event import MobilizonEvent
|
from mobilizon_reshare.event.event import MobilizonEvent
|
||||||
from mobilizon_reshare.publishers.abstract import AbstractPublisher
|
from mobilizon_reshare.publishers.abstract import (
|
||||||
|
AbstractPlatform,
|
||||||
|
AbstractEventFormatter,
|
||||||
|
)
|
||||||
from mobilizon_reshare.publishers.exceptions import PublisherError, InvalidResponse
|
from mobilizon_reshare.publishers.exceptions import PublisherError, InvalidResponse
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_event():
|
def test_event():
|
||||||
|
|
||||||
now = arrow.now()
|
now = arrow.now()
|
||||||
return MobilizonEvent(
|
return MobilizonEvent(
|
||||||
**{
|
**{
|
||||||
|
@ -26,66 +28,78 @@ def test_event():
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_publisher_valid(event):
|
def mock_formatter_valid():
|
||||||
class MockPublisher(AbstractPublisher):
|
class MockFormatter(AbstractEventFormatter):
|
||||||
def validate_event(self) -> None:
|
def validate_event(self, event) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_message_from_event(self) -> str:
|
def get_message_from_event(self, event) -> str:
|
||||||
return self.event.description
|
return event.description
|
||||||
|
|
||||||
def validate_credentials(self) -> None:
|
def validate_message(self, event) -> None:
|
||||||
pass
|
|
||||||
|
|
||||||
def validate_message(self) -> None:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _send(self, message):
|
def _send(self, message):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _validate_response(self, response) -> None:
|
return MockFormatter()
|
||||||
pass
|
|
||||||
|
|
||||||
return MockPublisher(event)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_publisher_invalid(event):
|
def mock_formatter_invalid():
|
||||||
class MockPublisher(AbstractPublisher):
|
class MockFormatter(AbstractEventFormatter):
|
||||||
def validate_event(self) -> None:
|
def validate_event(self, event) -> None:
|
||||||
raise PublisherError("Invalid event")
|
raise PublisherError("Invalid event")
|
||||||
|
|
||||||
def get_message_from_event(self) -> str:
|
def get_message_from_event(self, event) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def validate_credentials(self) -> None:
|
def validate_message(self, event) -> None:
|
||||||
raise PublisherError("Invalid credentials")
|
|
||||||
|
|
||||||
def validate_message(self) -> None:
|
|
||||||
raise PublisherError("Invalid message")
|
raise PublisherError("Invalid message")
|
||||||
|
|
||||||
def _send(self, message):
|
return MockFormatter()
|
||||||
pass
|
|
||||||
|
|
||||||
def _validate_response(self, response) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return MockPublisher(event)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_publisher_invalid_response(mock_publisher_invalid, event):
|
def mock_publisher_valid():
|
||||||
class MockPublisher(type(mock_publisher_invalid)):
|
class MockPublisher(AbstractPlatform):
|
||||||
def validate_event(self) -> None:
|
def _send(self, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _validate_response(self, response):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def validate_credentials(self) -> None:
|
def validate_credentials(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def validate_message(self) -> None:
|
return MockPublisher()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_publisher_invalid():
|
||||||
|
class MockPublisher(AbstractPlatform):
|
||||||
|
def _send(self, message):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _validate_response(self, response) -> None:
|
def _validate_response(self, response):
|
||||||
|
return InvalidResponse("error")
|
||||||
|
|
||||||
|
def validate_credentials(self) -> None:
|
||||||
|
raise PublisherError("error")
|
||||||
|
|
||||||
|
return MockPublisher()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_publisher_invalid_response():
|
||||||
|
class MockPublisher(AbstractPlatform):
|
||||||
|
def _send(self, message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _validate_response(self, response):
|
||||||
raise InvalidResponse("Invalid response")
|
raise InvalidResponse("Invalid response")
|
||||||
|
|
||||||
return MockPublisher(event)
|
def validate_credentials(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return MockPublisher()
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
def test_are_credentials_valid(test_event, mock_publisher_valid):
|
|
||||||
assert mock_publisher_valid.are_credentials_valid()
|
|
||||||
|
|
||||||
|
|
||||||
def test_are_credentials_valid_false(mock_publisher_invalid):
|
|
||||||
assert not mock_publisher_invalid.are_credentials_valid()
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_event_valid(mock_publisher_valid):
|
|
||||||
assert mock_publisher_valid.is_event_valid()
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_event_valid_false(mock_publisher_invalid):
|
|
||||||
assert not mock_publisher_invalid.is_event_valid()
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_message_valid(mock_publisher_valid):
|
|
||||||
assert mock_publisher_valid.is_message_valid()
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_message_valid_false(mock_publisher_invalid):
|
|
||||||
assert not mock_publisher_invalid.is_message_valid()
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
def test_is_event_valid(mock_formatter_valid, event):
|
||||||
|
assert mock_formatter_valid.is_event_valid(event)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_event_valid_false(mock_formatter_invalid, event):
|
||||||
|
assert not mock_formatter_invalid.is_event_valid(event)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_message_valid(mock_formatter_valid, event):
|
||||||
|
assert mock_formatter_valid.is_message_valid(event)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_message_valid_false(mock_formatter_invalid, event):
|
||||||
|
assert not mock_formatter_invalid.is_message_valid(event)
|
|
@ -3,17 +3,19 @@ from uuid import UUID
|
||||||
import pytest
|
import pytest
|
||||||
from asynctest import MagicMock
|
from asynctest import MagicMock
|
||||||
|
|
||||||
from mobilizon_reshare.config.config import get_settings
|
|
||||||
from mobilizon_reshare.event.event import MobilizonEvent
|
from mobilizon_reshare.event.event import MobilizonEvent
|
||||||
from mobilizon_reshare.models.publication import PublicationStatus, Publication
|
from mobilizon_reshare.models.publication import (
|
||||||
|
PublicationStatus,
|
||||||
|
Publication as PublicationModel,
|
||||||
|
)
|
||||||
from mobilizon_reshare.models.publisher import Publisher
|
from mobilizon_reshare.models.publisher import Publisher
|
||||||
|
from mobilizon_reshare.publishers.abstract import EventPublication
|
||||||
from mobilizon_reshare.publishers.coordinator import (
|
from mobilizon_reshare.publishers.coordinator import (
|
||||||
PublisherCoordinatorReport,
|
PublisherCoordinatorReport,
|
||||||
PublicationReport,
|
PublicationReport,
|
||||||
PublisherCoordinator,
|
PublisherCoordinator,
|
||||||
PublicationFailureNotifiersCoordinator,
|
PublicationFailureNotifiersCoordinator,
|
||||||
)
|
)
|
||||||
from mobilizon_reshare.publishers.telegram import TelegramPublisher
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -37,15 +39,20 @@ def test_publication_report_successful(statuses, successful):
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def mock_publication(
|
async def mock_publications(
|
||||||
|
num_publications: int,
|
||||||
test_event: MobilizonEvent,
|
test_event: MobilizonEvent,
|
||||||
|
mock_publisher_valid,
|
||||||
|
mock_formatter_valid,
|
||||||
):
|
):
|
||||||
|
result = []
|
||||||
|
for i in range(num_publications):
|
||||||
event = test_event.to_model()
|
event = test_event.to_model()
|
||||||
await event.save()
|
await event.save()
|
||||||
publisher = Publisher(name="telegram")
|
publisher = Publisher(name="telegram")
|
||||||
await publisher.save()
|
await publisher.save()
|
||||||
publication = Publication(
|
publication = PublicationModel(
|
||||||
id=UUID(int=1),
|
id=UUID(int=i + 1),
|
||||||
status=PublicationStatus.WAITING,
|
status=PublicationStatus.WAITING,
|
||||||
event=event,
|
event=event,
|
||||||
publisher=publisher,
|
publisher=publisher,
|
||||||
|
@ -53,21 +60,17 @@ async def mock_publication(
|
||||||
reason=None,
|
reason=None,
|
||||||
)
|
)
|
||||||
await publication.save()
|
await publication.save()
|
||||||
return publication
|
publication = EventPublication.from_orm(publication, test_event)
|
||||||
|
publication.publisher = mock_publisher_valid
|
||||||
|
publication.formatter = mock_formatter_valid
|
||||||
|
result.append(publication)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("num_publications", [2])
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_coordinator_run_success(
|
async def test_coordinator_run_success(mock_publications,):
|
||||||
test_event, mock_publication, mock_publisher_valid
|
coordinator = PublisherCoordinator(publications=mock_publications,)
|
||||||
):
|
|
||||||
coordinator = PublisherCoordinator(
|
|
||||||
test_event, {UUID(int=1): mock_publication, UUID(int=2): mock_publication}
|
|
||||||
)
|
|
||||||
coordinator.publishers_by_publication_id = {
|
|
||||||
UUID(int=1): mock_publisher_valid,
|
|
||||||
UUID(int=2): mock_publisher_valid,
|
|
||||||
}
|
|
||||||
|
|
||||||
report = coordinator.run()
|
report = coordinator.run()
|
||||||
assert len(report.reports) == 2
|
assert len(report.reports) == 2
|
||||||
assert report.successful, "\n".join(
|
assert report.successful, "\n".join(
|
||||||
|
@ -75,14 +78,15 @@ async def test_coordinator_run_success(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("num_publications", [1])
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_coordinator_run_failure(
|
async def test_coordinator_run_failure(
|
||||||
test_event, mock_publication, mock_publisher_invalid
|
mock_publications, mock_publisher_invalid, mock_formatter_invalid
|
||||||
):
|
):
|
||||||
coordinator = PublisherCoordinator(test_event, {UUID(int=1): mock_publication})
|
for pub in mock_publications:
|
||||||
coordinator.publishers_by_publication_id = {
|
pub.publisher = mock_publisher_invalid
|
||||||
UUID(int=1): mock_publisher_invalid,
|
pub.formatter = mock_formatter_invalid
|
||||||
}
|
coordinator = PublisherCoordinator(mock_publications)
|
||||||
|
|
||||||
report = coordinator.run()
|
report = coordinator.run()
|
||||||
assert len(report.reports) == 1
|
assert len(report.reports) == 1
|
||||||
|
@ -93,15 +97,15 @@ async def test_coordinator_run_failure(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("num_publications", [1])
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_coordinator_run_failure_response(
|
async def test_coordinator_run_failure_response(
|
||||||
test_event, mock_publication, mock_publisher_invalid_response
|
mock_publications, mock_publisher_invalid_response
|
||||||
):
|
):
|
||||||
coordinator = PublisherCoordinator(test_event, {UUID(int=1): mock_publication})
|
|
||||||
coordinator.publishers_by_publication_id = {
|
|
||||||
UUID(int=1): mock_publisher_invalid_response,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for pub in mock_publications:
|
||||||
|
pub.publisher = mock_publisher_invalid_response
|
||||||
|
coordinator = PublisherCoordinator(publications=mock_publications)
|
||||||
report = coordinator.run()
|
report = coordinator.run()
|
||||||
assert len(report.reports) == 1
|
assert len(report.reports) == 1
|
||||||
assert not report.successful
|
assert not report.successful
|
||||||
|
@ -109,39 +113,18 @@ async def test_coordinator_run_failure_response(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_notifier_coordinator_publication_failed(
|
async def test_notifier_coordinator_publication_failed(mock_publisher_valid):
|
||||||
test_event, mock_publisher_valid
|
|
||||||
):
|
|
||||||
mock_send = MagicMock()
|
mock_send = MagicMock()
|
||||||
mock_publisher_valid._send = mock_send
|
mock_publisher_valid._send = mock_send
|
||||||
report = PublisherCoordinatorReport(
|
report = PublicationReport(
|
||||||
{UUID(int=1): mock_publisher_valid, UUID(int=2): mock_publisher_valid},
|
|
||||||
{
|
|
||||||
UUID(int=1): PublicationReport(
|
|
||||||
status=PublicationStatus.FAILED,
|
status=PublicationStatus.FAILED,
|
||||||
reason="some failure",
|
reason="some failure",
|
||||||
publication_id=UUID(int=1),
|
publication_id=UUID(int=1),
|
||||||
),
|
|
||||||
UUID(int=2): PublicationReport(
|
|
||||||
status=PublicationStatus.FAILED,
|
|
||||||
reason="some failure",
|
|
||||||
publication_id=UUID(int=2),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
coordinator = PublicationFailureNotifiersCoordinator(test_event, report)
|
coordinator = PublicationFailureNotifiersCoordinator(
|
||||||
coordinator.notifiers = {
|
report, [mock_publisher_valid, mock_publisher_valid]
|
||||||
UUID(int=1): mock_publisher_valid,
|
)
|
||||||
UUID(int=2): mock_publisher_valid,
|
coordinator.notify_failure()
|
||||||
}
|
|
||||||
coordinator.notify_failures()
|
|
||||||
|
|
||||||
# 4 = 2 reports * 2 notifiers
|
# 4 = 2 reports * 2 notifiers
|
||||||
assert mock_send.call_count == 4
|
assert mock_send.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_get_formatted_message(test_event):
|
|
||||||
settings = get_settings()
|
|
||||||
settings.update({"publisher.telegram.msg_template_path": None})
|
|
||||||
message = PublisherCoordinator.get_formatted_message(test_event, "telegram")
|
|
||||||
assert message == TelegramPublisher(test_event).get_message_from_event()
|
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import responses
|
import responses
|
||||||
|
|
||||||
from mobilizon_reshare.config.config import get_settings
|
from mobilizon_reshare.config.config import get_settings
|
||||||
from mobilizon_reshare.models.publication import PublicationStatus
|
from mobilizon_reshare.models.publication import PublicationStatus
|
||||||
from mobilizon_reshare.publishers import get_active_publishers
|
from mobilizon_reshare.publishers import get_active_publishers
|
||||||
|
from mobilizon_reshare.publishers.abstract import EventPublication
|
||||||
from mobilizon_reshare.publishers.coordinator import PublisherCoordinator
|
from mobilizon_reshare.publishers.coordinator import PublisherCoordinator
|
||||||
from mobilizon_reshare.storage.query import (
|
from mobilizon_reshare.storage.query import (
|
||||||
get_publishers,
|
get_publishers,
|
||||||
update_publishers,
|
update_publishers,
|
||||||
publications_with_status,
|
publications_with_status,
|
||||||
get_all_events,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
api_uri = "https://zulip.twc-italia.org/api/v1/"
|
api_uri = "https://zulip.twc-italia.org/api/v1/"
|
||||||
users_me = {
|
users_me = {
|
||||||
"result": "success",
|
"result": "success",
|
||||||
|
@ -39,10 +40,7 @@ users_me = {
|
||||||
def mocked_valid_response():
|
def mocked_valid_response():
|
||||||
with responses.RequestsMock() as rsps:
|
with responses.RequestsMock() as rsps:
|
||||||
rsps.add(
|
rsps.add(
|
||||||
responses.GET,
|
responses.GET, api_uri + "users/me", json=users_me, status=200,
|
||||||
api_uri + "users/me",
|
|
||||||
json=users_me,
|
|
||||||
status=200,
|
|
||||||
)
|
)
|
||||||
rsps.add(
|
rsps.add(
|
||||||
responses.POST,
|
responses.POST,
|
||||||
|
@ -69,10 +67,7 @@ def mocked_credential_error_response():
|
||||||
def mocked_client_error_response():
|
def mocked_client_error_response():
|
||||||
with responses.RequestsMock() as rsps:
|
with responses.RequestsMock() as rsps:
|
||||||
rsps.add(
|
rsps.add(
|
||||||
responses.GET,
|
responses.GET, api_uri + "users/me", json=users_me, status=200,
|
||||||
api_uri + "users/me",
|
|
||||||
json=users_me,
|
|
||||||
status=200,
|
|
||||||
)
|
)
|
||||||
rsps.add(
|
rsps.add(
|
||||||
responses.POST,
|
responses.POST,
|
||||||
|
@ -105,11 +100,17 @@ async def setup_db(event_model_generator, publication_model_generator):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_zulip_publisher(mocked_valid_response, setup_db):
|
async def test_zulip_publisher(mocked_valid_response, setup_db, event):
|
||||||
|
publication_models = await publications_with_status(
|
||||||
|
status=PublicationStatus.WAITING
|
||||||
|
)
|
||||||
report = PublisherCoordinator(
|
report = PublisherCoordinator(
|
||||||
list(await get_all_events())[0],
|
list(
|
||||||
await publications_with_status(status=PublicationStatus.WAITING),
|
map(
|
||||||
|
partial(EventPublication.from_orm, event=event),
|
||||||
|
publication_models.values(),
|
||||||
|
)
|
||||||
|
)
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
assert list(report.reports.values())[0].status == PublicationStatus.COMPLETED
|
assert list(report.reports.values())[0].status == PublicationStatus.COMPLETED
|
||||||
|
@ -117,11 +118,18 @@ async def test_zulip_publisher(mocked_valid_response, setup_db):
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_zulip_publishr_failure_invalid_credentials(
|
async def test_zulip_publishr_failure_invalid_credentials(
|
||||||
mocked_credential_error_response, setup_db
|
mocked_credential_error_response, setup_db, event
|
||||||
):
|
):
|
||||||
|
publication_models = await publications_with_status(
|
||||||
|
status=PublicationStatus.WAITING
|
||||||
|
)
|
||||||
report = PublisherCoordinator(
|
report = PublisherCoordinator(
|
||||||
list(await get_all_events())[0],
|
list(
|
||||||
await publications_with_status(status=PublicationStatus.WAITING),
|
map(
|
||||||
|
partial(EventPublication.from_orm, event=event),
|
||||||
|
publication_models.values(),
|
||||||
|
)
|
||||||
|
)
|
||||||
).run()
|
).run()
|
||||||
assert list(report.reports.values())[0].status == PublicationStatus.FAILED
|
assert list(report.reports.values())[0].status == PublicationStatus.FAILED
|
||||||
assert list(report.reports.values())[0].reason == "Invalid credentials"
|
assert list(report.reports.values())[0].reason == "Invalid credentials"
|
||||||
|
@ -129,11 +137,18 @@ async def test_zulip_publishr_failure_invalid_credentials(
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_zulip_publishr_failure_client_error(
|
async def test_zulip_publishr_failure_client_error(
|
||||||
mocked_client_error_response, setup_db
|
mocked_client_error_response, setup_db, event
|
||||||
):
|
):
|
||||||
|
publication_models = await publications_with_status(
|
||||||
|
status=PublicationStatus.WAITING
|
||||||
|
)
|
||||||
report = PublisherCoordinator(
|
report = PublisherCoordinator(
|
||||||
list(await get_all_events())[0],
|
list(
|
||||||
await publications_with_status(status=PublicationStatus.WAITING),
|
map(
|
||||||
|
partial(EventPublication.from_orm, event=event),
|
||||||
|
publication_models.values(),
|
||||||
|
)
|
||||||
|
)
|
||||||
).run()
|
).run()
|
||||||
assert list(report.reports.values())[0].status == PublicationStatus.FAILED
|
assert list(report.reports.values())[0].status == PublicationStatus.FAILED
|
||||||
assert list(report.reports.values())[0].reason == "400 Error - Invalid request"
|
assert list(report.reports.values())[0].reason == "400 Error - Invalid request"
|
||||||
|
|
|
@ -48,10 +48,7 @@ two_publishers_specification = {"publisher": ["telegram", "twitter"]}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_update_publishers(
|
async def test_update_publishers(
|
||||||
specification,
|
specification, names, expected_result, generate_models,
|
||||||
names,
|
|
||||||
expected_result,
|
|
||||||
generate_models,
|
|
||||||
):
|
):
|
||||||
await generate_models(specification)
|
await generate_models(specification)
|
||||||
await update_publishers(names)
|
await update_publishers(names)
|
||||||
|
@ -71,6 +68,7 @@ async def test_update_publishers(
|
||||||
[
|
[
|
||||||
complete_specification,
|
complete_specification,
|
||||||
PublisherCoordinatorReport(
|
PublisherCoordinatorReport(
|
||||||
|
publications=[],
|
||||||
reports={
|
reports={
|
||||||
UUID(int=3): PublicationReport(
|
UUID(int=3): PublicationReport(
|
||||||
status=PublicationStatus.FAILED,
|
status=PublicationStatus.FAILED,
|
||||||
|
@ -83,7 +81,6 @@ async def test_update_publishers(
|
||||||
publication_id=UUID(int=4),
|
publication_id=UUID(int=4),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
publishers={},
|
|
||||||
),
|
),
|
||||||
MobilizonEvent(
|
MobilizonEvent(
|
||||||
name="event_1",
|
name="event_1",
|
||||||
|
@ -110,17 +107,12 @@ async def test_update_publishers(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_save_publication_report(
|
async def test_save_publication_report(
|
||||||
specification,
|
specification, report, event, expected_result, generate_models,
|
||||||
report,
|
|
||||||
event,
|
|
||||||
expected_result,
|
|
||||||
generate_models,
|
|
||||||
):
|
):
|
||||||
await generate_models(specification)
|
await generate_models(specification)
|
||||||
|
|
||||||
publications = await publications_with_status(
|
publications = await publications_with_status(
|
||||||
status=PublicationStatus.WAITING,
|
status=PublicationStatus.WAITING, event_mobilizon_id=event.mobilizon_id,
|
||||||
event_mobilizon_id=event.mobilizon_id,
|
|
||||||
)
|
)
|
||||||
await save_publication_report(report, publications)
|
await save_publication_report(report, publications)
|
||||||
publication_ids = set(report.reports.keys())
|
publication_ids = set(report.reports.keys())
|
||||||
|
|
Loading…
Reference in New Issue