extend cli (#48)

* fixed teardown

* Updated statuses management, tests (#41)

* Updated statuses management, tests

* storage: query: Generalize event loading logic.

* reformat

* storage: query: Rename load_events to prefetch_event_relations.

Co-authored-by: Giacomo Leidi <goodoldpaul@autistici.org>

* fixed test_window_no_event

* added strategy tests

* removed unused code

* added config tests

* added more config tests

* refactored tests

* updated pytest

* added inspect_all

* temp

* added colors

* storage: events_with_status: Use `EventPublicationStatus`.

* storage: events_with_status: Enable closed ranges.

* added time window to CLI

Co-authored-by: SlyK182 <60148777+SlyK182@users.noreply.github.com>
Co-authored-by: Giacomo Leidi <goodoldpaul@autistici.org>
This commit is contained in:
Simone Robutti 2021-08-04 18:53:58 +02:00 committed by GitHub
parent 929e3aa78e
commit 880a34115f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 244 additions and 52 deletions

View File

@ -1,21 +0,0 @@
import asyncio
import click
from mobilizon_bots.main import main
@click.group()
def mobilizon_bots():
pass
@mobilizon_bots.command()
@click.option("--settings-file", type=click.Path(exists=True))
def start(settings_file):
asyncio.run(main([settings_file] if settings_file else None))
if __name__ == "__main__":
mobilizon_bots()

View File

@ -0,0 +1,39 @@
import asyncio
import logging
import traceback
from logging.config import dictConfig
from pathlib import Path
from mobilizon_bots.config.config import update_settings_files
from mobilizon_bots.storage.db import tear_down, MobilizonBotsDB
logger = logging.getLogger(__name__)
async def graceful_exit(code):
await tear_down()
exit(code)
async def init(settings_file):
settings = update_settings_files(settings_file)
dictConfig(settings["logging"])
db_path = Path(settings.db_path)
db = MobilizonBotsDB(db_path)
await db.setup()
async def _safe_execution(f, settings_file):
await init(settings_file)
return_code = 1
try:
return_code = await f()
except Exception:
traceback.print_exc()
finally:
logger.debug("Closing")
await graceful_exit(return_code)
def safe_execution(f, settings_file):
asyncio.run(_safe_execution(f, settings_file))

106
mobilizon_bots/cli/cli.py Normal file
View File

@ -0,0 +1,106 @@
import functools
import click
from arrow import Arrow
from click import pass_context, pass_obj
from mobilizon_bots.cli import safe_execution
from mobilizon_bots.cli.inspect import inspect_events
from mobilizon_bots.cli.main import main
from mobilizon_bots.event.event import EventPublicationStatus
settings_file_option = click.option("--settings-file", type=click.Path(exists=True))
from_date_option = click.option("--begin", type=click.DateTime(), expose_value=True)
to_date_option = click.option("--end", type=click.DateTime(), expose_value=True)
@click.group()
def mobilizon_bots():
pass
@mobilizon_bots.command()
@settings_file_option
def start(settings_file):
safe_execution(main, settings_file=settings_file)
@mobilizon_bots.group()
@from_date_option
@to_date_option
@pass_context
def inspect(ctx, begin, end):
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
def all(settings_file):
safe_execution(inspect_events, settings_file)
@inspect.command()
@settings_file_option
@pass_obj
def waiting(obj, settings_file):
safe_execution(
functools.partial(
inspect_events,
EventPublicationStatus.WAITING,
frm=obj["begin"],
to=obj["end"],
),
settings_file,
)
@inspect.command()
@settings_file_option
@pass_obj
def failed(obj, settings_file):
safe_execution(
functools.partial(
inspect_events,
EventPublicationStatus.FAILED,
frm=obj["begin"],
to=obj["end"],
),
settings_file,
)
@inspect.command()
@settings_file_option
@pass_obj
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,
)
if __name__ == "__main__":
mobilizon_bots()

View File

@ -0,0 +1,44 @@
from typing import Iterable
import click
from arrow import Arrow
from mobilizon_bots.event.event import EventPublicationStatus
from mobilizon_bots.event.event import MobilizonEvent
from mobilizon_bots.storage.query import get_all_events
from mobilizon_bots.storage.query import events_with_status
status_to_color = {
EventPublicationStatus.COMPLETED: "green",
EventPublicationStatus.FAILED: "red",
EventPublicationStatus.PARTIAL: "yellow",
EventPublicationStatus.WAITING: "white",
}
def show_events(events: Iterable[MobilizonEvent]):
click.echo_via_pager("\n".join(map(pretty, events)))
def pretty(event: MobilizonEvent):
return (
f"{event.name}|{click.style(event.status.name, fg=status_to_color[event.status])}"
f"|{event.mobilizon_id}|{event.publication_time['telegram'].isoformat()}"
)
async def inspect_events(
status: EventPublicationStatus = None, frm: Arrow = None, to: Arrow = None
):
events = (
await events_with_status([status], from_date=frm, to_date=to)
if status
else await get_all_events(from_date=frm, to_date=to)
)
if events:
show_events(events)
else:
click.echo(f"No event found with status: {status}")

View File

@ -1,12 +1,10 @@
import logging.config
from pathlib import Path
from mobilizon_bots.config.config import update_settings_files
from mobilizon_bots.event.event_selection_strategies import select_event_to_publish
from mobilizon_bots.mobilizon.events import get_unpublished_events
from mobilizon_bots.publishers import get_active_publishers
from mobilizon_bots.publishers.coordinator import PublisherCoordinator
from mobilizon_bots.storage.db import MobilizonBotsDB, tear_down
from mobilizon_bots.storage.db import tear_down
from mobilizon_bots.storage.query import get_published_events, create_unpublished_events
logger = logging.getLogger(__name__)
@ -17,19 +15,14 @@ async def graceful_exit(code):
exit(code)
async def main(settings_file):
async def main():
"""
STUB
:return:
"""
settings = update_settings_files(settings_file)
logging.config.dictConfig(settings["logging"])
active_publishers = get_active_publishers()
db = MobilizonBotsDB(Path(settings.db_path))
await db.setup()
# Load past events
published_events = list(await get_published_events())

View File

@ -103,7 +103,6 @@ class CustomConfig:
def __new__(cls, settings_files: List[str] = None):
if cls._instance is None:
print("Creating the object")
cls._instance = super(CustomConfig, cls).__new__(cls)
cls.settings = build_and_validate_settings(settings_files)
return cls._instance

View File

@ -1,11 +1,11 @@
from typing import Iterable, Optional, List
import sys
from typing import Iterable, Optional
from arrow import Arrow
from tortoise.queryset import QuerySet
from tortoise.transactions import atomic
from mobilizon_bots.event.event import MobilizonEvent
from mobilizon_bots.event.event import MobilizonEvent, EventPublicationStatus
from mobilizon_bots.models.event import Event
from mobilizon_bots.models.publication import Publication, PublicationStatus
from mobilizon_bots.models.publisher import Publisher
@ -18,7 +18,7 @@ from mobilizon_bots.publishers.coordinator import PublisherCoordinatorReport
CONNECTION_NAME = "models" if "pytest" in sys.modules else None
async def prefetch_event_relations(queryset: QuerySet[Event]) -> list[Event]:
async def prefetch_event_relations(queryset: QuerySet[Event]) -> List[Event]:
return (
await queryset.prefetch_related("publications__publisher")
.order_by("begin_datetime")
@ -26,26 +26,54 @@ async def prefetch_event_relations(queryset: QuerySet[Event]) -> list[Event]:
)
def _add_date_window(
query, from_date: Optional[Arrow] = None, to_date: Optional[Arrow] = None,
):
if from_date:
query = query.filter(end_datetime__gt=from_date.datetime)
if to_date:
query = query.filter(end_datetime__lt=to_date.datetime)
return query
async def events_with_status(
statuses: list[PublicationStatus],
status: List[EventPublicationStatus],
from_date: Optional[Arrow] = None,
to_date: Optional[Arrow] = None,
) -> Iterable[MobilizonEvent]:
def _filter_event_with_status(event: Event) -> bool:
# This computes the status client-side instead of running in the DB. It shouldn't pose a performance problem
# in the short term, but should be moved to the query if possible.
event_status = MobilizonEvent.compute_status(list(event.publications))
return event_status in status
query = Event.all()
_add_date_window(query, from_date, to_date)
return map(
MobilizonEvent.from_model,
await prefetch_event_relations(Event.filter(publications__status__in=statuses)),
filter(_filter_event_with_status, await prefetch_event_relations(query),),
)
async def get_published_events() -> Iterable[MobilizonEvent]:
async def get_all_events(
from_date: Optional[Arrow] = None, to_date: Optional[Arrow] = None,
) -> Iterable[MobilizonEvent]:
return map(
MobilizonEvent.from_model,
await prefetch_event_relations(
Event.filter(publications__status=PublicationStatus.COMPLETED)
_add_date_window(Event.all(), from_date, to_date)
),
)
async def get_published_events() -> Iterable[MobilizonEvent]:
return await events_with_status([EventPublicationStatus.COMPLETED])
async def get_unpublished_events() -> Iterable[MobilizonEvent]:
return await events_with_status([PublicationStatus.WAITING])
return await events_with_status([EventPublicationStatus.WAITING])
async def save_event(event):
@ -59,9 +87,7 @@ async def save_publication(publisher_name, event_model, status: PublicationStatu
publisher = await Publisher.filter(name=publisher_name).first()
await Publication.create(
status=status,
event_id=event_model.id,
publisher_id=publisher.id,
status=status, event_id=event_model.id, publisher_id=publisher.id,
)

View File

@ -27,4 +27,4 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
mobilizon-bots="mobilizon_bots.cli:mobilizon_bots"
mobilizon-bots="mobilizon_bots.cli.cli:mobilizon_bots"

View File

@ -34,9 +34,11 @@ def setup():
event_1 = event_model_generator(begin_date=today)
event_2 = event_model_generator(idx=2, begin_date=today + timedelta(days=2))
event_3 = event_model_generator(idx=3, begin_date=today + timedelta(days=-2))
event_4 = event_model_generator(idx=4, begin_date=today + timedelta(days=-4))
await event_1.save()
await event_2.save()
await event_3.save()
await event_4.save()
publication_1 = publication_model_generator(
event_id=event_1.id, publisher_id=publisher_1.id
@ -56,13 +58,19 @@ def setup():
publisher_id=publisher_2.id,
status=PublicationStatus.WAITING,
)
publication_5 = publication_model_generator(
event_id=event_4.id,
publisher_id=publisher_2.id,
status=PublicationStatus.COMPLETED,
)
await publication_1.save()
await publication_2.save()
await publication_3.save()
await publication_4.save()
await publication_5.save()
return (
[event_1, event_2, event_3],
[publication_1, publication_2, publication_3, publication_4],
[event_1, event_2, event_3, event_4],
[publication_1, publication_2, publication_3, publication_4, publication_5],
[publisher_1, publisher_2],
today,
)
@ -81,9 +89,9 @@ async def test_get_published_events(
published_events = list(await get_published_events())
assert len(published_events) == 1
assert published_events[0].mobilizon_id == events[0].mobilizon_id
assert published_events[0].mobilizon_id == events[3].mobilizon_id
assert published_events[0].begin_datetime == arrow.get(today)
assert published_events[0].begin_datetime == arrow.get(today + timedelta(days=-4))
@pytest.mark.asyncio
@ -95,12 +103,10 @@ async def test_get_unpublished_events(
)
unpublished_events = list(await get_unpublished_events())
assert len(unpublished_events) == 2
assert len(unpublished_events) == 1
assert unpublished_events[0].mobilizon_id == events[2].mobilizon_id
assert unpublished_events[1].mobilizon_id == events[0].mobilizon_id
assert unpublished_events[0].begin_datetime == events[2].begin_datetime
assert unpublished_events[1].begin_datetime == events[0].begin_datetime
@pytest.mark.asyncio