format event cli (#61)

* added get_formatted_event

* fixed cli settings

* added format command

* refactored inspect
This commit is contained in:
Simone Robutti 2021-09-09 23:04:19 +02:00 committed by GitHub
parent 394d84b0a5
commit 360740eae0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 152 additions and 178 deletions

View File

@ -4,7 +4,7 @@ import traceback
from logging.config import dictConfig from logging.config import dictConfig
from pathlib import Path from pathlib import Path
from mobilizon_reshare.config.config import update_settings_files from mobilizon_reshare.config.config import get_settings
from mobilizon_reshare.storage.db import tear_down, MoReDB from mobilizon_reshare.storage.db import tear_down, MoReDB
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,7 +16,7 @@ async def graceful_exit(code):
async def init(settings_file): async def init(settings_file):
settings = update_settings_files(settings_file) settings = get_settings(settings_file)
dictConfig(settings["logging"]) dictConfig(settings["logging"])
db_path = Path(settings.db_path) db_path = Path(settings.db_path)
db = MoReDB(db_path) db = MoReDB(db_path)

View File

@ -2,9 +2,10 @@ import functools
import click import click
from arrow import Arrow from arrow import Arrow
from click import pass_context, pass_obj from click import pass_context
from mobilizon_reshare.cli import safe_execution from mobilizon_reshare.cli import safe_execution
from mobilizon_reshare.cli.format import format_event
from mobilizon_reshare.cli.inspect_event import inspect_events from mobilizon_reshare.cli.inspect_event import inspect_events
from mobilizon_reshare.cli.main import main from mobilizon_reshare.cli.main import main
from mobilizon_reshare.event.event import EventPublicationStatus from mobilizon_reshare.event.event import EventPublicationStatus
@ -35,88 +36,36 @@ def start(settings_file):
safe_execution(main, settings_file=settings_file) safe_execution(main, settings_file=settings_file)
@mobilizon_reshare.group() @mobilizon_reshare.command()
@from_date_option @from_date_option
@to_date_option @to_date_option
@click.argument("target", type=str)
@settings_file_option
@pass_context @pass_context
def inspect(ctx, begin, end): def inspect(ctx, target, begin, end, settings_file):
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.obj["begin"] = Arrow.fromdatetime(begin) if begin else None begin = Arrow.fromdatetime(begin) if begin else None
ctx.obj["end"] = Arrow.fromdatetime(end) if end else None end = Arrow.fromdatetime(end) if end else None
pass target_to_status = {
"waiting": EventPublicationStatus.WAITING,
"completed": EventPublicationStatus.COMPLETED,
@inspect.command() "failed": EventPublicationStatus.FAILED,
@settings_file_option "partial": EventPublicationStatus.PARTIAL,
@pass_obj "all": None,
def all(obj, settings_file): }
safe_execution( safe_execution(
functools.partial( functools.partial(inspect_events, target_to_status[target], frm=begin, to=end,),
inspect_events,
frm=obj["begin"],
to=obj["end"],
),
settings_file, settings_file,
) )
@inspect.command() @mobilizon_reshare.command()
@pass_obj
@settings_file_option @settings_file_option
def waiting(obj, settings_file): @click.argument("event-id", type=str)
@click.argument("publisher", type=str)
def format(settings_file, event_id, publisher):
safe_execution( safe_execution(
functools.partial( functools.partial(format_event, event_id, publisher), settings_file,
inspect_events,
EventPublicationStatus.WAITING,
frm=obj["begin"],
to=obj["end"],
),
settings_file,
)
@inspect.command()
@pass_obj
@settings_file_option
def failed(obj, settings_file):
safe_execution(
functools.partial(
inspect_events,
EventPublicationStatus.FAILED,
frm=obj["begin"],
to=obj["end"],
),
settings_file,
)
@inspect.command()
@pass_obj
@settings_file_option
def partial(obj, settings_file):
safe_execution(
functools.partial(
inspect_events,
EventPublicationStatus.PARTIAL,
frm=obj["begin"],
to=obj["end"],
),
settings_file,
)
@inspect.command()
@settings_file_option
@pass_obj
def completed(obj, settings_file):
safe_execution(
functools.partial(
inspect_events,
EventPublicationStatus.COMPLETED,
frm=obj["begin"],
to=obj["end"],
),
settings_file,
) )

View File

@ -0,0 +1,17 @@
import click
from mobilizon_reshare.event.event import MobilizonEvent
from mobilizon_reshare.models.event import Event
from mobilizon_reshare.publishers.coordinator import PublisherCoordinator
async def format_event(event_id, publisher):
event = await Event.get_or_none(mobilizon_id=event_id).prefetch_related(
"publications__publisher"
)
if not event:
click.echo(f"Event with mobilizon_id {event_id} not found.")
return
event = MobilizonEvent.from_model(event)
message = PublisherCoordinator.get_formatted_message(event, publisher)
click.echo(message)

View File

@ -16,11 +16,7 @@ base_validators = [
# strategy to decide events to publish # strategy to decide events to publish
Validator("selection.strategy", must_exist=True, is_type_of=str), Validator("selection.strategy", must_exist=True, is_type_of=str),
Validator( Validator(
"publishing.window.begin", "publishing.window.begin", must_exist=True, is_type_of=int, gte=0, lte=24,
must_exist=True,
is_type_of=int,
gte=0,
lte=24,
), ),
Validator("publishing.window.end", must_exist=True, is_type_of=int, gte=0, lte=24), Validator("publishing.window.end", must_exist=True, is_type_of=int, gte=0, lte=24),
# url of the main Mobilizon instance to download events from # url of the main Mobilizon instance to download events from
@ -61,21 +57,15 @@ def build_settings(
with importlib.resources.path( with importlib.resources.path(
mobilizon_reshare, "settings.toml" mobilizon_reshare, "settings.toml"
) as bundled_settings_path: ) as bundled_settings_path:
SETTINGS_FILE = ( SETTINGS_FILE = [
[ bundled_settings_path,
bundled_settings_path, Path(dirs.site_config_dir, "mobilizon_reshare.toml"),
Path(dirs.site_config_dir, "mobilizon_reshare.toml"), Path(dirs.user_config_dir, "mobilizon_reshare.toml"),
Path(dirs.user_config_dir, "mobilizon_reshare.toml"), os.environ.get("MOBILIZION_RESHARE_SETTINGS_FILE"),
os.environ.get("MOBILIZION_RESHARE_SETTINGS_FILE"), settings_file,
settings_file, ]
]
# FIXME: This is needed because otherwise dynaconf would load the bundled settings.toml file.
if os.environ.get("ENV_FOR_DYNACONF", "") != "testing"
else [settings_file]
)
ENVVAR_PREFIX = "MOBILIZON_RESHARE" ENVVAR_PREFIX = "MOBILIZON_RESHARE"
return Dynaconf( return Dynaconf(
environments=True, environments=True,
envvar_prefix=ENVVAR_PREFIX, envvar_prefix=ENVVAR_PREFIX,
@ -123,11 +113,21 @@ def build_and_validate_settings(settings_file: Optional[str] = None):
# better in the future. # better in the future.
class CustomConfig: class CustomConfig:
_instance = None _instance = None
_settings_file = None
def __new__(cls, settings_file: Optional[str] = None): def __new__(cls, settings_file: Optional[str] = None):
if cls._instance is None: if (
settings_file is None and cls._settings_file is not None
): # normal access, I don't want to reload
return cls._instance
if (
cls._instance is None and cls._settings_file is None
) or settings_file != cls._settings_file:
cls._settings_file = settings_file
cls._instance = super(CustomConfig, cls).__new__(cls) cls._instance = super(CustomConfig, cls).__new__(cls)
cls.settings = build_and_validate_settings(settings_file) cls.settings = build_and_validate_settings(settings_file)
return cls._instance return cls._instance
def update(self, settings_file: Optional[str] = None): def update(self, settings_file: Optional[str] = None):
@ -137,8 +137,3 @@ class CustomConfig:
def get_settings(settings_file: Optional[str] = None): def get_settings(settings_file: Optional[str] = None):
config = CustomConfig(settings_file) config = CustomConfig(settings_file)
return config.settings return config.settings
def update_settings_files(settings_file: Optional[str] = None):
CustomConfig().update(settings_file)
return CustomConfig().settings

View File

@ -46,7 +46,7 @@ class AbstractNotifier(ABC):
"Abstract classes cannot access notifiers/publishers' settings" "Abstract classes cannot access notifiers/publishers' settings"
) )
try: try:
t, n = cls._conf or tuple() # Avoid unpacking ``None`` t, n = cls._conf or tuple()
return get_settings()[t][n] return get_settings()[t][n]
except (KeyError, ValueError): except (KeyError, ValueError):
raise InvalidAttribute( raise InvalidAttribute(

View File

@ -12,13 +12,14 @@ from mobilizon_reshare.publishers.telegram import TelegramPublisher
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
name_to_publisher_class = {"telegram": TelegramPublisher}
class BuildPublisherMixin: class BuildPublisherMixin:
@staticmethod @staticmethod
def build_publishers( def build_publishers(
event: MobilizonEvent, publisher_names event: MobilizonEvent, publisher_names
) -> dict[str, AbstractPublisher]: ) -> dict[str, AbstractPublisher]:
name_to_publisher_class = {"telegram": TelegramPublisher}
return { return {
publisher_name: name_to_publisher_class[publisher_name](event) publisher_name: name_to_publisher_class[publisher_name](event)
@ -112,6 +113,19 @@ class PublisherCoordinator(BuildPublisherMixin):
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(BuildPublisherMixin): class AbstractNotifiersCoordinator(BuildPublisherMixin):
def __init__(self, event: MobilizonEvent): def __init__(self, event: MobilizonEvent):

View File

@ -45,3 +45,4 @@ encoding = "utf8"
[default.logging.root] [default.logging.root]
level = "DEBUG" level = "DEBUG"
handlers = ['console', 'file'] handlers = ['console', 'file']

View File

@ -1,6 +1,6 @@
import pkg_resources import pkg_resources
from mobilizon_reshare.config.config import get_settings, update_settings_files from mobilizon_reshare.config.config import get_settings
def test_singleton(): def test_singleton():
@ -12,12 +12,22 @@ def test_singleton():
assert id(config_1) == id(config_2) assert id(config_1) == id(config_2)
def test_singleton_update(): def test_same_file():
settings_file = pkg_resources.resource_filename( settings_file = pkg_resources.resource_filename(
"tests.resources.config", "test_singleton.toml" "tests.resources.config", "test_singleton.toml"
) )
config_1 = get_settings(settings_file) config_1 = get_settings(settings_file)
config_2 = update_settings_files(settings_file) config_2 = get_settings(settings_file)
config_3 = get_settings() assert id(config_1) == id(config_2)
def test_singleton_new_file():
settings_file = pkg_resources.resource_filename(
"tests.resources.config", "test_singleton.toml"
)
settings_file_2 = pkg_resources.resource_filename(
"tests.resources.config", "test_singleton_2.toml"
)
config_1 = get_settings(settings_file)
config_2 = get_settings(settings_file_2)
assert id(config_1) != id(config_2) assert id(config_1) != id(config_2)
assert id(config_2) == id(config_3)

View File

@ -4,7 +4,7 @@ import dynaconf
import pkg_resources import pkg_resources
import pytest import pytest
from mobilizon_reshare.config.config import update_settings_files from mobilizon_reshare.config.config import get_settings
@pytest.fixture @pytest.fixture
@ -15,53 +15,30 @@ def invalid_settings_file(tmp_path, toml_content):
@pytest.mark.parametrize("toml_content", ["invalid toml["]) @pytest.mark.parametrize("toml_content", ["invalid toml["])
def test_update_failure_invalid_toml(invalid_settings_file): def test_get_settings_failure_invalid_toml(invalid_settings_file):
with pytest.raises(dynaconf.vendor.toml.decoder.TomlDecodeError): with pytest.raises(dynaconf.vendor.toml.decoder.TomlDecodeError):
update_settings_files(invalid_settings_file.absolute()) get_settings(invalid_settings_file.absolute())
@pytest.mark.parametrize("toml_content", [""]) @pytest.mark.parametrize("toml_content", [""])
def test_update_failure_invalid_preliminary_config(invalid_settings_file): def test_get_settings_failure_invalid_preliminary_config(invalid_settings_file):
os.environ["SECRETS_FOR_DYNACONF"] = "" os.environ["SECRETS_FOR_DYNACONF"] = ""
with pytest.raises(dynaconf.validator.ValidationError): with pytest.raises(dynaconf.validator.ValidationError):
update_settings_files(invalid_settings_file.absolute()) get_settings(invalid_settings_file.absolute())
@pytest.mark.parametrize( @pytest.mark.parametrize(
"invalid_toml,pattern_in_exception", "invalid_toml,pattern_in_exception",
[ [["config_with_invalid_strategy.toml", "break_between_events_in_minutes"]],
["config_with_invalid_strategy.toml", "break_between_events_in_minutes"],
["config_with_invalid_mobilizon.toml", "mobilizon"],
["config_with_preliminary.toml", "publishing.window.begin"],
],
) )
def test_update_failure_config_base_validators(invalid_toml, pattern_in_exception): def test_get_settings_failure_config_base_validators(
invalid_toml, pattern_in_exception
):
with pytest.raises(dynaconf.validator.ValidationError) as e: with pytest.raises(dynaconf.validator.ValidationError) as e:
update_settings_files( get_settings(
pkg_resources.resource_filename("tests.resources.config", invalid_toml) pkg_resources.resource_filename("tests.resources.config", invalid_toml)
) )
assert e.match(pattern_in_exception)
@pytest.mark.parametrize(
"invalid_toml,pattern_in_exception",
[
["empty.toml", "publisher.*.active"],
["config_with_invalid_telegram.toml", "token"],
],
)
def test_update_failure_config_all_validators(invalid_toml, pattern_in_exception):
os.environ["SECRETS_FOR_DYNACONF"] = pkg_resources.resource_filename(
"tests.resources.config", invalid_toml
)
with pytest.raises(dynaconf.validator.ValidationError) as e:
update_settings_files(
pkg_resources.resource_filename(
"tests.resources.config", "test_singleton.toml"
)
)
assert e.match(pattern_in_exception) assert e.match(pattern_in_exception)

View File

@ -145,9 +145,7 @@ def event_model_generator():
@pytest.fixture() @pytest.fixture()
def publisher_model_generator(): def publisher_model_generator():
def _publisher_model_generator( def _publisher_model_generator(idx=1,):
idx=1,
):
return Publisher(name=f"publisher_{idx}", account_ref=f"account_ref_{idx}") return Publisher(name=f"publisher_{idx}", account_ref=f"account_ref_{idx}")
return _publisher_model_generator return _publisher_model_generator

View File

@ -3,6 +3,7 @@ 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
from mobilizon_reshare.models.publisher import Publisher from mobilizon_reshare.models.publisher import Publisher
@ -12,6 +13,7 @@ from mobilizon_reshare.publishers.coordinator import (
PublisherCoordinator, PublisherCoordinator,
PublicationFailureNotifiersCoordinator, PublicationFailureNotifiersCoordinator,
) )
from mobilizon_reshare.publishers.telegram import TelegramPublisher
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -35,9 +37,7 @@ def test_publication_report_successful(statuses, successful):
@pytest.fixture @pytest.fixture
@pytest.mark.asyncio @pytest.mark.asyncio
async def mock_publication( async def mock_publication(test_event: MobilizonEvent,):
test_event: MobilizonEvent,
):
event = test_event.to_model() event = test_event.to_model()
await event.save() await event.save()
publisher = Publisher(name="telegram") publisher = Publisher(name="telegram")
@ -136,3 +136,10 @@ async def test_notifier_coordinator_publication_failed(
# 4 = 2 reports * 2 notifiers # 4 = 2 reports * 2 notifiers
assert mock_send.call_count == 4 assert mock_send.call_count == 4
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()

View File

@ -1,7 +0,0 @@
[default.publishing.window]
begin=12
end=18
[default.selection]
strategy = "next_event"

View File

@ -1,11 +1,11 @@
[default.source.mobilizon] [testing.source.mobilizon]
url="https://some_mobilizon" url="https://some_mobilizon"
group="my_group" group="my_group"
[default.publishing.window] [testing.publishing.window]
begin=12 begin=12
end=18 end=18
[default.selection] [testing.selection]
strategy = "next_event" strategy = "next_event"

View File

@ -1,46 +1,46 @@
[default.selection] [testing.selection]
strategy = "next_event" strategy = "next_event"
[default.source.mobilizon] [testing.source.mobilizon]
url="https://mobilizon.it/api" url="https://mobilizon.it/api"
group="tech_workers_coalition_italia" group="tech_workers_coalition_italia"
[default.publishing.window] [testing.publishing.window]
begin=12 begin=12
end=24 end=24
[default.selection.strategy_options] [testing.selection.strategy_options]
break_between_events_in_minutes =1 break_between_events_in_minutes =1
[default.publisher.telegram] [testing.publisher.telegram]
active=true active=true
chat_id="xxx" chat_id="xxx"
token="xxx" token="xxx"
username="xxx" username="xxx"
[default.publisher.facebook] [testing.publisher.facebook]
active=false active=false
[default.publisher.zulip] [testing.publisher.zulip]
active=false active=false
[default.publisher.twitter] [testing.publisher.twitter]
active=false active=false
[default.publisher.mastodon] [testing.publisher.mastodon]
active=false active=false
[default.notifier.telegram] [testing.notifier.telegram]
active=true active=true
chat_id="xxx" chat_id="xxx"
misspelled_token="xxx" misspelled_token="xxx"
username="xxx" username="xxx"
[default.notifier.zulip] [testing.notifier.zulip]
active=false active=false
[default.notifier.twitter] [testing.notifier.twitter]
active=false active=false
[default.notifier.mastodon] [testing.notifier.mastodon]
active=false active=false

View File

@ -1,5 +1,5 @@
[default.selection] [testing.selection]
strategy = "next_event" strategy = "next_event"
[default.selection.strategy_options] [testing.selection.strategy_options]
break_between_events_in_minutes = 60 break_between_events_in_minutes = 60

View File

@ -1,12 +1,12 @@
[default.source.mobilizon] [testing.source.mobilizon]
url="https://some_mobilizon" url="https://some_mobilizon"
group="my_group" group="my_group"
[default.publishing.window] [testing.publishing.window]
begin=12 begin=12
end=18 end=18
[default.selection] [testing.selection]
strategy = "next_event" strategy = "next_event"
[default.selection.strategy_options] [testing.selection.strategy_options]
break_between_events_in_minutes = 60 break_between_events_in_minutes = 60

View File

@ -1,13 +1,13 @@
[default.source.mobilizon] [testing.source.mobilizon]
url="https://some_mobilizon" url="https://some_mobilizon"
group="my_group" group="my_group"
[default.selection] [testing.selection]
strategy = "next_event" strategy = "next_event"
[default.publishing.window] [testing.publishing.window]
begin=12 begin=12
end=18 end=18
[default.selection.strategy_options] [testing.selection.strategy_options]
break_between_events_in_minutes =60 break_between_events_in_minutes =60

View File

@ -0,0 +1,13 @@
[testing.source.mobilizon]
url="https://some_mobilizon"
group="my_group"
[testing.selection]
strategy = "next_event"
[testing.publishing.window]
begin=12
end=18
[testing.selection.strategy_options]
break_between_events_in_minutes =60