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 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
logger = logging.getLogger(__name__)
@ -16,7 +16,7 @@ async def graceful_exit(code):
async def init(settings_file):
settings = update_settings_files(settings_file)
settings = get_settings(settings_file)
dictConfig(settings["logging"])
db_path = Path(settings.db_path)
db = MoReDB(db_path)

View File

@ -2,9 +2,10 @@ import functools
import click
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.format import format_event
from mobilizon_reshare.cli.inspect_event import inspect_events
from mobilizon_reshare.cli.main import main
from mobilizon_reshare.event.event import EventPublicationStatus
@ -35,88 +36,36 @@ def start(settings_file):
safe_execution(main, settings_file=settings_file)
@mobilizon_reshare.group()
@mobilizon_reshare.command()
@from_date_option
@to_date_option
@click.argument("target", type=str)
@settings_file_option
@pass_context
def inspect(ctx, begin, end):
def inspect(ctx, target, begin, end, settings_file):
ctx.ensure_object(dict)
ctx.obj["begin"] = Arrow.fromdatetime(begin) if begin else None
ctx.obj["end"] = Arrow.fromdatetime(end) if end else None
pass
@inspect.command()
@settings_file_option
@pass_obj
def all(obj, settings_file):
begin = Arrow.fromdatetime(begin) if begin else None
end = Arrow.fromdatetime(end) if end else None
target_to_status = {
"waiting": EventPublicationStatus.WAITING,
"completed": EventPublicationStatus.COMPLETED,
"failed": EventPublicationStatus.FAILED,
"partial": EventPublicationStatus.PARTIAL,
"all": None,
}
safe_execution(
functools.partial(
inspect_events,
frm=obj["begin"],
to=obj["end"],
),
functools.partial(inspect_events, target_to_status[target], frm=begin, to=end,),
settings_file,
)
@inspect.command()
@pass_obj
@mobilizon_reshare.command()
@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(
functools.partial(
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,
functools.partial(format_event, event_id, publisher), 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
Validator("selection.strategy", must_exist=True, is_type_of=str),
Validator(
"publishing.window.begin",
must_exist=True,
is_type_of=int,
gte=0,
lte=24,
"publishing.window.begin", 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
@ -61,21 +57,15 @@ def build_settings(
with importlib.resources.path(
mobilizon_reshare, "settings.toml"
) as bundled_settings_path:
SETTINGS_FILE = (
[
bundled_settings_path,
Path(dirs.site_config_dir, "mobilizon_reshare.toml"),
Path(dirs.user_config_dir, "mobilizon_reshare.toml"),
os.environ.get("MOBILIZION_RESHARE_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]
)
SETTINGS_FILE = [
bundled_settings_path,
Path(dirs.site_config_dir, "mobilizon_reshare.toml"),
Path(dirs.user_config_dir, "mobilizon_reshare.toml"),
os.environ.get("MOBILIZION_RESHARE_SETTINGS_FILE"),
settings_file,
]
ENVVAR_PREFIX = "MOBILIZON_RESHARE"
return Dynaconf(
environments=True,
envvar_prefix=ENVVAR_PREFIX,
@ -123,11 +113,21 @@ def build_and_validate_settings(settings_file: Optional[str] = None):
# better in the future.
class CustomConfig:
_instance = None
_settings_file = 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.settings = build_and_validate_settings(settings_file)
return cls._instance
def update(self, settings_file: Optional[str] = None):
@ -137,8 +137,3 @@ class CustomConfig:
def get_settings(settings_file: Optional[str] = None):
config = CustomConfig(settings_file)
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"
)
try:
t, n = cls._conf or tuple() # Avoid unpacking ``None``
t, n = cls._conf or tuple()
return get_settings()[t][n]
except (KeyError, ValueError):
raise InvalidAttribute(

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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():
@ -12,12 +12,22 @@ def test_singleton():
assert id(config_1) == id(config_2)
def test_singleton_update():
def test_same_file():
settings_file = pkg_resources.resource_filename(
"tests.resources.config", "test_singleton.toml"
)
config_1 = get_settings(settings_file)
config_2 = update_settings_files(settings_file)
config_3 = get_settings()
config_2 = get_settings(settings_file)
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_2) == id(config_3)

View File

@ -4,7 +4,7 @@ import dynaconf
import pkg_resources
import pytest
from mobilizon_reshare.config.config import update_settings_files
from mobilizon_reshare.config.config import get_settings
@pytest.fixture
@ -15,53 +15,30 @@ def invalid_settings_file(tmp_path, toml_content):
@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):
update_settings_files(invalid_settings_file.absolute())
get_settings(invalid_settings_file.absolute())
@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"] = ""
with pytest.raises(dynaconf.validator.ValidationError):
update_settings_files(invalid_settings_file.absolute())
get_settings(invalid_settings_file.absolute())
@pytest.mark.parametrize(
"invalid_toml,pattern_in_exception",
[
["config_with_invalid_strategy.toml", "break_between_events_in_minutes"],
["config_with_invalid_mobilizon.toml", "mobilizon"],
["config_with_preliminary.toml", "publishing.window.begin"],
],
[["config_with_invalid_strategy.toml", "break_between_events_in_minutes"]],
)
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:
update_settings_files(
get_settings(
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)

View File

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

View File

@ -3,6 +3,7 @@ from uuid import UUID
import pytest
from asynctest import MagicMock
from mobilizon_reshare.config.config import get_settings
from mobilizon_reshare.event.event import MobilizonEvent
from mobilizon_reshare.models.publication import PublicationStatus, Publication
from mobilizon_reshare.models.publisher import Publisher
@ -12,6 +13,7 @@ from mobilizon_reshare.publishers.coordinator import (
PublisherCoordinator,
PublicationFailureNotifiersCoordinator,
)
from mobilizon_reshare.publishers.telegram import TelegramPublisher
@pytest.mark.parametrize(
@ -35,9 +37,7 @@ def test_publication_report_successful(statuses, successful):
@pytest.fixture
@pytest.mark.asyncio
async def mock_publication(
test_event: MobilizonEvent,
):
async def mock_publication(test_event: MobilizonEvent,):
event = test_event.to_model()
await event.save()
publisher = Publisher(name="telegram")
@ -136,3 +136,10 @@ async def test_notifier_coordinator_publication_failed(
# 4 = 2 reports * 2 notifiers
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"
group="my_group"
[default.publishing.window]
[testing.publishing.window]
begin=12
end=18
[default.selection]
[testing.selection]
strategy = "next_event"

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
[default.source.mobilizon]
[testing.source.mobilizon]
url="https://some_mobilizon"
group="my_group"
[default.selection]
[testing.selection]
strategy = "next_event"
[default.publishing.window]
[testing.publishing.window]
begin=12
end=18
[default.selection.strategy_options]
[testing.selection.strategy_options]
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