diff --git a/mobilizon_reshare/cli/commands/format/format.py b/mobilizon_reshare/cli/commands/format/format.py index b3dbb58..8d69bbe 100644 --- a/mobilizon_reshare/cli/commands/format/format.py +++ b/mobilizon_reshare/cli/commands/format/format.py @@ -1,8 +1,8 @@ import click +from mobilizon_reshare.event.event import MobilizonEvent from mobilizon_reshare.models.event import Event from mobilizon_reshare.publishers.platforms.platform_mapping import get_formatter_class -from mobilizon_reshare.storage.query.converter import event_from_model async def format_event(event_id, publisher_name: str): @@ -12,6 +12,6 @@ async def format_event(event_id, publisher_name: str): if not event: click.echo(f"Event with mobilizon_id {event_id} not found.") return - event = event_from_model(event) + event = MobilizonEvent.from_model(event) message = get_formatter_class(publisher_name)().get_message_from_event(event) click.echo(message) diff --git a/mobilizon_reshare/event/event.py b/mobilizon_reshare/event/event.py index 6830e1e..8e294ad 100644 --- a/mobilizon_reshare/event/event.py +++ b/mobilizon_reshare/event/event.py @@ -7,6 +7,8 @@ import arrow from jinja2 import Template from mobilizon_reshare.config.config import get_settings +from mobilizon_reshare.models.event import Event +from mobilizon_reshare.models.publication import PublicationStatus, Publication class EventPublicationStatus(IntEnum): @@ -50,3 +52,70 @@ class MobilizonEvent: def format(self, pattern: Template) -> str: return self._fill_template(pattern) + + @classmethod + def from_model(cls, event: Event): + publication_status = cls._compute_event_status(list(event.publications)) + publication_time = {} + + for pub in event.publications: + if publication_status != EventPublicationStatus.WAITING: + assert pub.timestamp is not None + publication_time[pub.publisher.name] = arrow.get(pub.timestamp).to( + "local" + ) + return cls( + name=event.name, + description=event.description, + begin_datetime=arrow.get(event.begin_datetime).to("local"), + end_datetime=arrow.get(event.end_datetime).to("local"), + mobilizon_link=event.mobilizon_link, + mobilizon_id=event.mobilizon_id, + thumbnail_link=event.thumbnail_link, + location=event.location, + publication_time=publication_time, + status=publication_status, + last_update_time=arrow.get(event.last_update_time).to("local"), + ) + + def to_model(self, db_id: Optional[UUID] = None) -> Event: + + kwargs = { + "name": self.name, + "description": self.description, + "mobilizon_id": self.mobilizon_id, + "mobilizon_link": self.mobilizon_link, + "thumbnail_link": self.thumbnail_link, + "location": self.location, + "begin_datetime": self.begin_datetime.astimezone( + self.begin_datetime.tzinfo + ), + "end_datetime": self.end_datetime.astimezone(self.end_datetime.tzinfo), + "last_update_time": self.last_update_time.astimezone( + self.last_update_time.tzinfo + ), + } + if db_id is not None: + kwargs.update({"id": db_id}) + return Event(**kwargs) + + @staticmethod + def _compute_event_status( + publications: list[Publication], + ) -> EventPublicationStatus: + if not publications: + return EventPublicationStatus.WAITING + + unique_statuses: set[PublicationStatus] = set( + pub.status for pub in publications + ) + + if unique_statuses == { + PublicationStatus.COMPLETED, + PublicationStatus.FAILED, + }: + return EventPublicationStatus.PARTIAL + elif len(unique_statuses) == 1: + return EventPublicationStatus[unique_statuses.pop().name] + + raise ValueError(f"Illegal combination of PublicationStatus: {unique_statuses}") diff --git a/mobilizon_reshare/publishers/abstract.py b/mobilizon_reshare/publishers/abstract.py index a152084..cc65cbb 100644 --- a/mobilizon_reshare/publishers/abstract.py +++ b/mobilizon_reshare/publishers/abstract.py @@ -11,6 +11,7 @@ from jinja2 import Environment, FileSystemLoader, Template from mobilizon_reshare.config.config import get_settings from mobilizon_reshare.event.event import MobilizonEvent from .exceptions import InvalidAttribute +from ..models.publication import Publication JINJA_ENV = Environment(loader=FileSystemLoader("/")) @@ -188,6 +189,18 @@ class EventPublication(BasePublication): event: MobilizonEvent id: UUID + @classmethod + def from_orm(cls, model: Publication, 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(publisher, formatter, event, model.id,) + @dataclass class RecapPublication(BasePublication): diff --git a/mobilizon_reshare/storage/query/converter.py b/mobilizon_reshare/storage/query/converter.py deleted file mode 100644 index 7fe98a3..0000000 --- a/mobilizon_reshare/storage/query/converter.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import Optional -from uuid import UUID - -import arrow -import tortoise.timezone - -from mobilizon_reshare.event.event import EventPublicationStatus, MobilizonEvent -from mobilizon_reshare.models.event import Event -from mobilizon_reshare.models.publication import Publication, PublicationStatus -from mobilizon_reshare.publishers.abstract import EventPublication - - -def event_from_model(event: Event): - - publication_status = compute_event_status(list(event.publications)) - publication_time = {} - - for pub in event.publications: - if publication_status != EventPublicationStatus.WAITING: - assert pub.timestamp is not None - publication_time[pub.publisher.name] = arrow.get(pub.timestamp).to("local") - return MobilizonEvent( - name=event.name, - description=event.description, - begin_datetime=arrow.get(event.begin_datetime).to("local"), - end_datetime=arrow.get(event.end_datetime).to("local"), - mobilizon_link=event.mobilizon_link, - mobilizon_id=event.mobilizon_id, - thumbnail_link=event.thumbnail_link, - location=event.location, - publication_time=publication_time, - status=publication_status, - last_update_time=arrow.get(event.last_update_time).to("local"), - ) - - -def event_to_model(event: MobilizonEvent, db_id: Optional[UUID] = None) -> Event: - kwargs = { - "name": event.name, - "description": event.description, - "mobilizon_id": event.mobilizon_id, - "mobilizon_link": event.mobilizon_link, - "thumbnail_link": event.thumbnail_link, - "location": event.location, - "begin_datetime": event.begin_datetime.astimezone(event.begin_datetime.tzinfo), - "end_datetime": event.end_datetime.astimezone(event.end_datetime.tzinfo), - "last_update_time": event.last_update_time.astimezone( - event.last_update_time.tzinfo - ), - } - if db_id is not None: - kwargs.update({"id": db_id}) - return Event(**kwargs) - - -def compute_event_status(publications: list[Publication]) -> EventPublicationStatus: - if not publications: - return EventPublicationStatus.WAITING - - unique_statuses: set[PublicationStatus] = set(pub.status for pub in publications) - - if unique_statuses == { - PublicationStatus.COMPLETED, - PublicationStatus.FAILED, - }: - return EventPublicationStatus.PARTIAL - elif len(unique_statuses) == 1: - return EventPublicationStatus[unique_statuses.pop().name] - - raise ValueError(f"Illegal combination of PublicationStatus: {unique_statuses}") - - -def publication_from_orm(model: Publication, event: MobilizonEvent) -> EventPublication: - # 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 EventPublication(publisher, formatter, event, model.id,) diff --git a/mobilizon_reshare/storage/query/read.py b/mobilizon_reshare/storage/query/read.py index b485e36..2edc47b 100644 --- a/mobilizon_reshare/storage/query/read.py +++ b/mobilizon_reshare/storage/query/read.py @@ -12,11 +12,7 @@ from mobilizon_reshare.models.event import Event from mobilizon_reshare.models.publication import Publication, PublicationStatus from mobilizon_reshare.models.publisher import Publisher from mobilizon_reshare.publishers.abstract import EventPublication -from mobilizon_reshare.storage.query.converter import ( - event_from_model, - compute_event_status, - publication_from_orm, -) + from mobilizon_reshare.storage.query.exceptions import EventNotFound @@ -46,13 +42,13 @@ async def events_with_status( 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 = compute_event_status(list(event.publications)) + event_status = MobilizonEvent._compute_event_status(list(event.publications)) return event_status in status query = Event.all() return map( - event_from_model, + MobilizonEvent.from_model, filter( _filter_event_with_status, await prefetch_event_relations( @@ -73,7 +69,7 @@ async def get_all_publications( async def get_all_mobilizon_events( from_date: Optional[Arrow] = None, to_date: Optional[Arrow] = None, ) -> list[MobilizonEvent]: - return [event_from_model(event) for event in await get_all_events()] + return [MobilizonEvent.from_model(event) for event in await get_all_events()] async def get_all_events( @@ -140,7 +136,7 @@ async def events_without_publications( events = await prefetch_event_relations( _add_date_window(query, "begin_datetime", from_date, to_date) ) - return [event_from_model(event) for event in events] + return [MobilizonEvent.from_model(event) for event in events] async def get_event(event_mobilizon_id: UUID) -> Event: @@ -157,11 +153,11 @@ async def get_event_publications( mobilizon_event: MobilizonEvent, ) -> list[EventPublication]: event = await get_event(mobilizon_event.mobilizon_id) - return [publication_from_orm(p, mobilizon_event) for p in event.publications] + return [EventPublication.from_orm(p, mobilizon_event) for p in event.publications] async def get_mobilizon_event(event_mobilizon_id: UUID) -> MobilizonEvent: - return event_from_model(await get_event(event_mobilizon_id)) + return MobilizonEvent.from_model(await get_event(event_mobilizon_id)) async def get_publisher_by_name(name) -> Publisher: @@ -185,7 +181,7 @@ async def build_publications( await event_model.build_publication_by_publisher_name(name) for name in publishers ] - return [publication_from_orm(m, event) for m in models] + return [EventPublication.from_orm(m, event) for m in models] @atomic() @@ -201,9 +197,12 @@ async def get_failed_publications_for_event( ) for p in failed_publications: await p.fetch_related("publisher") - mobilizon_event = event_from_model(event) + mobilizon_event = MobilizonEvent.from_model(event) return list( - map(partial(publication_from_orm, event=mobilizon_event), failed_publications) + map( + partial(EventPublication.from_orm, event=mobilizon_event), + failed_publications, + ) ) @@ -215,8 +214,8 @@ async def get_publication(publication_id: UUID): ) # TODO: this is redundant but there's some prefetch problem otherwise publication.event = await get_event(publication.event.mobilizon_id) - return publication_from_orm( - event=event_from_model(publication.event), model=publication + return EventPublication.from_orm( + event=MobilizonEvent.from_model(publication.event), model=publication ) except DoesNotExist: return None diff --git a/mobilizon_reshare/storage/query/write.py b/mobilizon_reshare/storage/query/write.py index 98c492d..8032709 100644 --- a/mobilizon_reshare/storage/query/write.py +++ b/mobilizon_reshare/storage/query/write.py @@ -11,7 +11,6 @@ from mobilizon_reshare.models.publisher import Publisher from mobilizon_reshare.publishers.coordinators.event_publishing.publish import ( PublisherCoordinatorReport, ) -from mobilizon_reshare.storage.query.converter import event_to_model from mobilizon_reshare.storage.query.read import ( events_without_publications, is_known, @@ -79,14 +78,12 @@ async def create_unpublished_events( for event in events_from_mobilizon: if not await is_known(event): # Either an event is unknown - await event_to_model(event).save() + await event.to_model().save() else: # Or it's known and changed event_model = await get_event(event.mobilizon_id) if event.last_update_time > event_model.last_update_time: - await event_to_model(event=event, db_id=event_model.id).save( - force_update=True - ) + await event.to_model(db_id=event_model.id).save(force_update=True) # Or it's known and unchanged, in which case we do nothing. return await events_without_publications() diff --git a/tests/commands/test_format.py b/tests/commands/test_format.py index 5ba01e5..faf70bf 100644 --- a/tests/commands/test_format.py +++ b/tests/commands/test_format.py @@ -7,13 +7,12 @@ from mobilizon_reshare.publishers.platforms.platform_mapping import ( get_formatter_class, name_to_formatter_class, ) -from mobilizon_reshare.storage.query.converter import event_to_model @pytest.mark.parametrize("publisher_name", name_to_formatter_class.keys()) @pytest.mark.asyncio async def test_format_event(runner, event, capsys, publisher_name): - event_model = event_to_model(event) + event_model = event.to_model() await event_model.save() await format_event( event_id=str(event_model.mobilizon_id), publisher_name=publisher_name diff --git a/tests/commands/test_publish.py b/tests/commands/test_publish.py index 07639b4..23a55eb 100644 --- a/tests/commands/test_publish.py +++ b/tests/commands/test_publish.py @@ -3,8 +3,7 @@ from logging import DEBUG import pytest from mobilizon_reshare.main.publish import select_and_publish, publish_event -from mobilizon_reshare.storage.query.converter import event_from_model -from mobilizon_reshare.event.event import EventPublicationStatus +from mobilizon_reshare.event.event import EventPublicationStatus, MobilizonEvent from mobilizon_reshare.models.event import Event from mobilizon_reshare.models.publication import PublicationStatus from mobilizon_reshare.storage.query.read import get_all_publications @@ -75,7 +74,9 @@ async def test_select_and_publish_new_event( assert p.status == PublicationStatus.COMPLETED # the derived status for the event should be COMPLETED - assert event_from_model(event).status == EventPublicationStatus.COMPLETED + assert ( + MobilizonEvent.from_model(event).status == EventPublicationStatus.COMPLETED + ) @pytest.mark.asyncio diff --git a/tests/commands/test_start.py b/tests/commands/test_start.py index 93c3202..7db3757 100644 --- a/tests/commands/test_start.py +++ b/tests/commands/test_start.py @@ -3,10 +3,9 @@ from logging import DEBUG, INFO import pytest from mobilizon_reshare.config.command import CommandConfig -from mobilizon_reshare.storage.query.converter import event_from_model, event_to_model from mobilizon_reshare.storage.query.read import get_all_mobilizon_events from tests.commands.conftest import simple_event_element, second_event_element -from mobilizon_reshare.event.event import EventPublicationStatus +from mobilizon_reshare.event.event import EventPublicationStatus, MobilizonEvent from mobilizon_reshare.main.start import start from mobilizon_reshare.models.event import Event from mobilizon_reshare.models.publication import PublicationStatus @@ -86,7 +85,8 @@ async def test_start_new_event( # the derived status for the event should be COMPLETED assert ( - event_from_model(all_events[0]).status == EventPublicationStatus.COMPLETED + MobilizonEvent.from_model(all_events[0]).status + == EventPublicationStatus.COMPLETED ) @@ -107,7 +107,7 @@ async def test_start_event_from_db( command_config, ): event = event_generator() - event_model = event_to_model(event) + event_model = event.to_model() await event_model.save() with caplog.at_level(DEBUG): @@ -136,7 +136,10 @@ async def test_start_event_from_db( assert p.status == PublicationStatus.COMPLETED # the derived status for the event should be COMPLETED - assert event_from_model(event_model).status == EventPublicationStatus.COMPLETED + assert ( + MobilizonEvent.from_model(event_model).status + == EventPublicationStatus.COMPLETED + ) @pytest.mark.asyncio @@ -157,7 +160,7 @@ async def test_start_publisher_failure( command_config, ): event = event_generator() - event_model = event_to_model(event) + event_model = event.to_model() await event_model.save() with caplog.at_level(DEBUG): @@ -188,7 +191,10 @@ async def test_start_publisher_failure( for _ in range(2) ] # 2 publications failed * 2 notifiers # the derived status for the event should be FAILED - assert event_from_model(event_model).status == EventPublicationStatus.FAILED + assert ( + MobilizonEvent.from_model(event_model).status + == EventPublicationStatus.FAILED + ) @pytest.mark.asyncio diff --git a/tests/conftest.py b/tests/conftest.py index 48c996c..5a2af02 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,6 @@ from mobilizon_reshare.publishers.abstract import ( AbstractEventFormatter, ) from mobilizon_reshare.publishers.exceptions import PublisherError, InvalidResponse -from mobilizon_reshare.storage.query.converter import event_to_model from tests import today with importlib.resources.path( @@ -121,7 +120,7 @@ def event() -> MobilizonEvent: @pytest.fixture async def stored_event(event) -> Event: - model = event_to_model(event) + model = event.to_model() await model.save() await model.fetch_related("publications") return model diff --git a/tests/models/test_event.py b/tests/models/test_event.py index 03e234e..42329e1 100644 --- a/tests/models/test_event.py +++ b/tests/models/test_event.py @@ -5,14 +5,9 @@ import arrow import pytest import tortoise.timezone -from mobilizon_reshare.event.event import EventPublicationStatus +from mobilizon_reshare.event.event import EventPublicationStatus, MobilizonEvent from mobilizon_reshare.models.event import Event from mobilizon_reshare.models.publication import PublicationStatus -from mobilizon_reshare.storage.query.converter import ( - event_from_model, - event_to_model, - compute_event_status, -) @pytest.mark.asyncio @@ -93,7 +88,7 @@ async def test_event_sort_by_date(event_model_generator): @pytest.mark.asyncio async def test_mobilizon_event_to_model(event): - event_model = event_to_model(event) + event_model = event.to_model() await event_model.save() event_db = await Event.all().first() @@ -141,7 +136,7 @@ async def test_mobilizon_event_from_model( .prefetch_related("publications__publisher") .first() ) - event = event_from_model(event=event_db) + event = MobilizonEvent.from_model(event=event_db) begin_date_utc = arrow.Arrow(year=2021, month=1, day=1, hour=11, minute=30) @@ -196,4 +191,4 @@ async def test_mobilizon_event_compute_status_partial( ) await publication.save() publications.append(publication) - assert compute_event_status(publications) == expected_result + assert MobilizonEvent._compute_event_status(publications) == expected_result diff --git a/tests/publishers/test_coordinator.py b/tests/publishers/test_coordinator.py index a185fe9..195d46d 100644 --- a/tests/publishers/test_coordinator.py +++ b/tests/publishers/test_coordinator.py @@ -23,10 +23,6 @@ from mobilizon_reshare.publishers.coordinators.event_publishing.publish import ( from mobilizon_reshare.publishers.coordinators.recap_publishing.recap import ( RecapCoordinator, ) -from mobilizon_reshare.storage.query.converter import ( - event_to_model, - publication_from_orm, -) from tests import today @@ -96,7 +92,7 @@ async def mock_publications( ): result = [] for i in range(num_publications): - event = event_to_model(test_event) + event = test_event.to_model() await event.save() publisher = Publisher(name="telegram") await publisher.save() @@ -107,7 +103,7 @@ async def mock_publications( timestamp=today + timedelta(hours=i), reason=None, ) - publication = publication_from_orm(publication, test_event) + publication = EventPublication.from_orm(publication, test_event) publication.publisher = mock_publisher_valid publication.formatter = mock_formatter_valid result.append(publication) diff --git a/tests/publishers/test_zulip.py b/tests/publishers/test_zulip.py index 7d8f24d..89cc401 100644 --- a/tests/publishers/test_zulip.py +++ b/tests/publishers/test_zulip.py @@ -14,7 +14,6 @@ from mobilizon_reshare.publishers.exceptions import ( HTTPResponseError, ) from mobilizon_reshare.publishers.platforms.zulip import ZulipFormatter, ZulipPublisher -from mobilizon_reshare.storage.query.converter import event_to_model from mobilizon_reshare.storage.query.read import build_publications, get_all_publishers one_publication_specification = { @@ -103,7 +102,7 @@ async def setup_db(generate_models): @pytest.fixture @pytest.mark.asyncio async def unsaved_publications(setup_db, event): - await event_to_model(event).save() + await event.to_model().save() publishers = [p.name for p in await get_all_publishers()] return await build_publications(event, publishers) diff --git a/tests/storage/test_read_query.py b/tests/storage/test_read_query.py index 42c10ee..3464952 100644 --- a/tests/storage/test_read_query.py +++ b/tests/storage/test_read_query.py @@ -2,7 +2,6 @@ from uuid import UUID import pytest -from mobilizon_reshare.storage.query.converter import event_to_model from mobilizon_reshare.storage.query.read import get_all_mobilizon_events @@ -12,6 +11,6 @@ async def test_get_all_events(event_generator): event_generator(mobilizon_id=UUID(int=i), published=False) for i in range(4) ] for e in all_events: - await event_to_model(e).save() + await e.to_model().save() assert list(await get_all_mobilizon_events()) == all_events