diff --git a/mobilizon_reshare/cli/cli.py b/mobilizon_reshare/cli/cli.py index c79d389..5db5437 100644 --- a/mobilizon_reshare/cli/cli.py +++ b/mobilizon_reshare/cli/cli.py @@ -1,5 +1,4 @@ import functools -from enum import Enum import click from arrow import Arrow @@ -8,98 +7,156 @@ from click import pass_context from mobilizon_reshare.cli import safe_execution from mobilizon_reshare.cli.commands.format.format import format_event from mobilizon_reshare.cli.commands.inspect.inspect_event import inspect_events +from mobilizon_reshare.cli.commands.inspect.inspect_publication import ( + inspect_publications, +) from mobilizon_reshare.cli.commands.start.main import main as start_main from mobilizon_reshare.cli.commands.recap.main import main as recap_main +from mobilizon_reshare.config.config import current_version from mobilizon_reshare.config.publishers import publisher_names from mobilizon_reshare.event.event import EventPublicationStatus +from mobilizon_reshare.models.publication import PublicationStatus status_name_to_enum = { - "waiting": EventPublicationStatus.WAITING, - "completed": EventPublicationStatus.COMPLETED, - "failed": EventPublicationStatus.FAILED, - "partial": EventPublicationStatus.PARTIAL, - "all": None, + "event": { + "waiting": EventPublicationStatus.WAITING, + "completed": EventPublicationStatus.COMPLETED, + "failed": EventPublicationStatus.FAILED, + "partial": EventPublicationStatus.PARTIAL, + "all": None, + }, + "publication": { + "completed": PublicationStatus.COMPLETED, + "failed": PublicationStatus.FAILED, + "all": None, + }, } settings_file_option = click.option( + "-f", "--settings-file", type=click.Path(exists=True), help="The path for the settings file. " "Overrides the one specified in the environment variables.", ) from_date_option = click.option( + "-b", "--begin", type=click.DateTime(), expose_value=True, - help="Include only events that begin after this datetime", + help="Include only events that begin after this datetime.", ) to_date_option = click.option( + "-e", "--end", type=click.DateTime(), expose_value=True, - help="Include only events that begin before this datetime", + help="Include only events that begin before this datetime.", +) +event_status_option = click.option( + "-s", + "--status", + type=click.Choice(list(status_name_to_enum["event"].keys())), + default="all", + expose_value=True, + help="Include only events with the given STATUS.", +) +publication_status_option = click.option( + "-s", + "--status", + type=click.Choice(list(status_name_to_enum["publication"].keys())), + default="all", + expose_value=True, + help="Include only publications with the given STATUS.", ) -class InspectTarget(Enum): - ALL = "all" - WAITING = "waiting" - - def __str__(self): - return self.value +def print_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + click.echo(current_version()) + ctx.exit() @click.group() +@click.option( + "--version", is_flag=True, callback=print_version, expose_value=False, is_eager=True +) +@pass_context def mobilizon_reshare(): pass -@mobilizon_reshare.command(help="Synchronize and publish events") +@mobilizon_reshare.command(help="Synchronize and publish events.") @settings_file_option -def start(settings_file): - safe_execution(start_main, settings_file=settings_file) +def start(ctx, settings): + ctx.ensure_object(dict) + safe_execution(start_main, settings_file=settings) -@mobilizon_reshare.command(help="Publish a recap of already published events") +@mobilizon_reshare.command(help="Publish a recap of already published events.") @settings_file_option -def recap(settings_file): - safe_execution(recap_main, settings_file=settings_file) +def recap(settings): + safe_execution(recap_main, settings_file=settings) -@mobilizon_reshare.command(help="Print events in the database that are in STATUS") +@mobilizon_reshare.group(help="List objects in the database with different criteria.") @from_date_option @to_date_option -@click.argument( - "status", type=click.Choice(list(status_name_to_enum.keys())), -) +@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 + + +@inspect.command(help="Query for events in the database.") +@event_status_option @settings_file_option @pass_context -def inspect(ctx, status, begin, end, settings_file): +def event(ctx, status, settings): ctx.ensure_object(dict) - begin = Arrow.fromdatetime(begin) if begin else None - end = Arrow.fromdatetime(end) if end else None - safe_execution( functools.partial( - inspect_events, status_name_to_enum[status], frm=begin, to=end, + inspect_events, + status_name_to_enum["event"][status], + frm=ctx.obj["begin"], + to=ctx.obj["end"], ), - settings_file, + settings, + ) + + +@inspect.command(help="Query for publications in the database.") +@publication_status_option +@settings_file_option +@pass_context +def publication(ctx, status, settings): + ctx.ensure_object(dict) + safe_execution( + functools.partial( + inspect_publications, + status_name_to_enum["publication"][status], + frm=ctx.obj["begin"], + to=ctx.obj["end"], + ), + settings, ) @mobilizon_reshare.command( - help="Format and print event with mobilizon id EVENT-ID using the publisher's format named" - "PUBLISHER" + help="Format and print event with EVENT-ID using the publisher's format named " + "PUBLISHER." ) -@settings_file_option @click.argument("event-id", type=click.UUID) @click.argument("publisher", type=click.Choice(publisher_names)) -def format(settings_file, event_id, publisher): +@settings_file_option +def format(event_id, publisher, settings): safe_execution( - functools.partial(format_event, event_id, publisher), settings_file, + functools.partial(format_event, event_id, publisher), settings, ) if __name__ == "__main__": - mobilizon_reshare() + mobilizon_reshare(obj={}) diff --git a/mobilizon_reshare/cli/commands/inspect/inspect_event.py b/mobilizon_reshare/cli/commands/inspect/inspect_event.py index 2997888..43b0643 100644 --- a/mobilizon_reshare/cli/commands/inspect/inspect_event.py +++ b/mobilizon_reshare/cli/commands/inspect/inspect_event.py @@ -27,8 +27,8 @@ def show_events(events: Iterable[MobilizonEvent]): def pretty(event: MobilizonEvent): return ( - f"{event.name}|{click.style(event.status.name, fg=status_to_color[event.status])}" - f"|{event.mobilizon_id}|{event.begin_datetime.isoformat()}->{event.end_datetime.isoformat()}" + f"{event.name : ^40}{click.style(event.status.name, fg=status_to_color[event.status]) : ^22}" + f"{str(event.mobilizon_id) : <40}{event.begin_datetime.isoformat() : <29}{event.end_datetime.isoformat()}" ) diff --git a/mobilizon_reshare/cli/commands/inspect/inspect_publication.py b/mobilizon_reshare/cli/commands/inspect/inspect_publication.py new file mode 100644 index 0000000..4a77770 --- /dev/null +++ b/mobilizon_reshare/cli/commands/inspect/inspect_publication.py @@ -0,0 +1,41 @@ +from typing import Iterable + +import click +from arrow import Arrow + +from mobilizon_reshare.models.publication import Publication, PublicationStatus +from mobilizon_reshare.storage.query.read import ( + get_all_publications, + publications_with_status, +) + +status_to_color = { + PublicationStatus.COMPLETED: "green", + PublicationStatus.FAILED: "red", +} + + +def show_publications(publications: Iterable[Publication]): + click.echo_via_pager("\n".join(map(pretty, publications))) + + +def pretty(publication: Publication): + return ( + f"{str(publication.id) : <40}{publication.timestamp.isoformat() : <36}" + f"{click.style(publication.status.name, fg=status_to_color[publication.status]) : <22}" + f"{publication.publisher.name : <12}{str(publication.event.id)}" + ) + + +async def inspect_publications( + status: PublicationStatus = None, frm: Arrow = None, to: Arrow = None +): + if status is None: + publications = await get_all_publications(from_date=frm, to_date=to) + else: + publications = await publications_with_status(status, from_date=frm, to_date=to) + + if publications: + show_publications(list(publications)) + else: + click.echo(f"No publication found with status: {status}") diff --git a/mobilizon_reshare/models/publication.py b/mobilizon_reshare/models/publication.py index 927146e..bf954ec 100644 --- a/mobilizon_reshare/models/publication.py +++ b/mobilizon_reshare/models/publication.py @@ -13,9 +13,7 @@ class Publication(Model): id = fields.UUIDField(pk=True) status = fields.IntEnumField(PublicationStatus) - # When a Publication's status is WAITING - # we don't need a timestamp nor a reason - timestamp = fields.DatetimeField(null=True) + timestamp = fields.DatetimeField() reason = fields.TextField(null=True) event = fields.ForeignKeyField("models.Event", related_name="publications") diff --git a/mobilizon_reshare/storage/query/read.py b/mobilizon_reshare/storage/query/read.py index 67a9bf6..2991d60 100644 --- a/mobilizon_reshare/storage/query/read.py +++ b/mobilizon_reshare/storage/query/read.py @@ -55,6 +55,14 @@ async def events_with_status( ) +async def get_all_publications( + from_date: Optional[Arrow] = None, to_date: Optional[Arrow] = None, +) -> Iterable[Publication]: + return await prefetch_publication_relations( + _add_date_window(Publication.all(), "timestamp", from_date, to_date) + ) + + async def get_all_events( from_date: Optional[Arrow] = None, to_date: Optional[Arrow] = None, ) -> Iterable[MobilizonEvent]: @@ -74,6 +82,14 @@ async def prefetch_event_relations(queryset: QuerySet[Event]) -> list[Event]: ) +async def prefetch_publication_relations(queryset: QuerySet[Publication]) -> list[Publication]: + return ( + await queryset.prefetch_related("publisher", "event") + .order_by("timestamp") + .distinct() + ) + + def _add_date_window( query, field_name: str, @@ -93,7 +109,7 @@ async def publications_with_status( event_mobilizon_id: Optional[UUID] = None, from_date: Optional[Arrow] = None, to_date: Optional[Arrow] = None, -) -> dict[UUID, Publication]: +) -> Iterable[Publication]: query = Publication.filter(status=status) if event_mobilizon_id: @@ -101,12 +117,9 @@ async def publications_with_status( event__mobilizon_id=event_mobilizon_id ) - query = _add_date_window(query, "timestamp", from_date, to_date) - - publications_list = ( - await query.prefetch_related("publisher").order_by("timestamp").distinct() + return await prefetch_publication_relations( + _add_date_window(query, "timestamp", from_date, to_date) ) - return {pub.id: pub for pub in publications_list} async def events_without_publications( diff --git a/tests/publishers/test_coordinator.py b/tests/publishers/test_coordinator.py index ceaf1ae..cb59e50 100644 --- a/tests/publishers/test_coordinator.py +++ b/tests/publishers/test_coordinator.py @@ -1,4 +1,5 @@ import logging +from datetime import timedelta from uuid import UUID import pytest @@ -18,6 +19,7 @@ from mobilizon_reshare.publishers.coordinator import ( PublicationFailureNotifiersCoordinator, RecapCoordinator, ) +from tests import today @pytest.fixture() @@ -91,7 +93,7 @@ async def mock_publications( id=UUID(int=i + 1), event=event, publisher=publisher, - timestamp=None, + timestamp=today + timedelta(hours=i), reason=None, ) publication = EventPublication.from_orm(publication, test_event) diff --git a/tests/storage/test_query.py b/tests/storage/test_query.py index d42007c..dbbbf93 100644 --- a/tests/storage/test_query.py +++ b/tests/storage/test_query.py @@ -5,7 +5,6 @@ import arrow import pytest from mobilizon_reshare.event.event import MobilizonEvent, EventPublicationStatus -from mobilizon_reshare.models.event import Event from mobilizon_reshare.models.publication import PublicationStatus from mobilizon_reshare.storage.query.read import ( get_published_events, @@ -83,7 +82,7 @@ async def test_publications_with_status( to_date=to_date, ) - assert publications == {pub.id: pub for pub in expected_result} + assert publications == expected_result @pytest.mark.asyncio