publication window (#24)

* added get events stub

* added event parsing

* removed datetime

* added config and tests

* fixed response format in test

* more tests

* added error handling for request

* improved dummy data

* added get_unpublished_events

* added test

* removed last_accessed

* mobilizon_url moved to fixture

* added config comments

* moved mobilizon group to config

* added method

* removed non-working test settings

* added publishing window logic and tests

* added inner/outer window check and mocking

* improved fixture

* fixed outer window

* fixed tests
This commit is contained in:
Simone Robutti 2021-07-07 11:19:37 +02:00 committed by GitHub
parent 6cea51bcab
commit 40919a994f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 145 additions and 45 deletions

View File

@ -42,6 +42,16 @@ def build_and_validate_settings(settings_files: List[str] = None):
[
# 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,
),
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
Validator("source.mobilizon.url", must_exist=True, is_type_of=str),
Validator("source.mobilizon.group", must_exist=True, is_type_of=str),

View File

@ -2,7 +2,7 @@ from dynaconf import Validator
telegram_validators = [
Validator("publisher.telegram.chat_id", must_exist=True),
Validator("publisher.telegram.msg_template_path", must_exist=True),
Validator("publisher.telegram.msg_template_path", must_exist=True,),
Validator("publisher.telegram.token", must_exist=True),
Validator("publisher.telegram.username", must_exist=True),
]

View File

@ -2,16 +2,35 @@ from abc import ABC, abstractmethod
from typing import List, Optional
import arrow
from mobilizon_bots.config.config import settings
from mobilizon_bots.event.event import MobilizonEvent
class EventSelectionStrategy(ABC):
@abstractmethod
def select(
self,
published_events: List[MobilizonEvent],
unpublished_events: List[MobilizonEvent],
publisher_name: str,
) -> Optional[MobilizonEvent]:
if not self.is_in_publishing_window():
return None
return self._select(published_events, unpublished_events)
def is_in_publishing_window(self) -> bool:
window_beginning = settings["publishing"]["window"]["begin"]
window_end = settings["publishing"]["window"]["end"]
now_hour = arrow.now().datetime.hour
if window_beginning <= window_end:
return window_beginning <= now_hour < window_end
else:
return now_hour >= window_beginning or now_hour < window_end
@abstractmethod
def _select(
self,
published_events: List[MobilizonEvent],
unpublished_events: List[MobilizonEvent],
) -> Optional[MobilizonEvent]:
pass
@ -22,7 +41,7 @@ class SelectNextEventStrategy(EventSelectionStrategy):
minimum_break_between_events_in_minutes
)
def select(
def _select(
self,
published_events: List[MobilizonEvent],
unpublished_events: List[MobilizonEvent],
@ -62,4 +81,4 @@ class EventSelector:
def select_event_to_publish(
self, strategy: EventSelectionStrategy
) -> Optional[MobilizonEvent]:
return strategy.select(self.published_events, self.unpublished_events)
return strategy._select(self.published_events, self.unpublished_events)

View File

@ -13,8 +13,13 @@ group="my_group"
[default.selection]
strategy = "next_event"
[default.publishing.window]
begin=12
end=18
[default.selection.strategy_options]
break_between_events_in_minutes =5
break_between_events_in_minutes =60
[default.publisher.telegram]
active=true
@ -69,9 +74,3 @@ encoding = "utf8"
[default.logging.root]
level = "DEBUG"
handlers = ['console', 'file']
[testing]
DEBUG = true
LOCAL_STATE_DIR = "/tmp/mobots"
LOG_DIR = "/tmp/mobots"
DB_PATH = ":memory:"

28
poetry.lock generated
View File

@ -11,7 +11,7 @@ typing_extensions = ">=3.7.2"
[[package]]
name = "arrow"
version = "1.1.0"
version = "1.1.1"
description = "Better dates & times for Python"
category = "main"
optional = false
@ -150,11 +150,11 @@ python-versions = ">=3.5"
[[package]]
name = "packaging"
version = "20.9"
version = "21.0"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2"
@ -293,7 +293,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tortoise-orm"
version = "0.17.3"
version = "0.17.4"
description = "Easy async ORM for python, built with relations in mind"
category = "main"
optional = false
@ -322,7 +322,7 @@ python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.5"
version = "1.26.6"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
@ -344,7 +344,7 @@ python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "f11fdba0a8d8fed33e35ec9b95f5768a7fc7ded4c06e009624469b6d5ab6762e"
content-hash = "0879accc273f3fc21de4ca7a01682392ed44f1173b17b1e09e884a5b2c7ad51d"
[metadata.files]
aiosqlite = [
@ -352,8 +352,8 @@ aiosqlite = [
{file = "aiosqlite-0.16.1.tar.gz", hash = "sha256:2e915463164efa65b60fd1901aceca829b6090082f03082618afca6fb9c8fdf7"},
]
arrow = [
{file = "arrow-1.1.0-py3-none-any.whl", hash = "sha256:8cbe6a629b1c54ae11b52d6d9e70890089241958f63bc59467e277e34b7a5378"},
{file = "arrow-1.1.0.tar.gz", hash = "sha256:b8fe13abf3517abab315e09350c903902d1447bd311afbc17547ba1cb3ff5bd8"},
{file = "arrow-1.1.1-py3-none-any.whl", hash = "sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510"},
{file = "arrow-1.1.1.tar.gz", hash = "sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a"},
]
asynctest = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
@ -440,8 +440,8 @@ more-itertools = [
{file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"},
]
packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
@ -488,8 +488,8 @@ six = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
tortoise-orm = [
{file = "tortoise-orm-0.17.3.tar.gz", hash = "sha256:6e5e56694b64118faaada2670343c909d6c9f84c7235f3372b8a398b2e8d6628"},
{file = "tortoise_orm-0.17.3-py3-none-any.whl", hash = "sha256:99b448a870a81b6edb3ef9d2f0e22b2c83afa2b6348178840d3ccdbf03e206d3"},
{file = "tortoise-orm-0.17.4.tar.gz", hash = "sha256:8314a9ae63d3f009bac5da3e7d1f7e3f2de8f9bad43ce1efcd3e059209cd3f9d"},
{file = "tortoise_orm-0.17.4-py3-none-any.whl", hash = "sha256:f052b6089e30748afec88669f1a1cf01a3662cdac81cf5427dfb338839ad6027"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
@ -497,8 +497,8 @@ typing-extensions = [
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
]
urllib3 = [
{file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"},
{file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"},
{file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"},
{file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},

View File

@ -17,7 +17,7 @@ click = "^8.0.1"
[tool.poetry.dev-dependencies]
asynctest = "^0.13"
pytest = "^5.3"
responses = "^0.13"
responses = "^0.13.3"
pytest-asyncio = "^0.10"
[build-system]
@ -25,4 +25,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:mobilizon_bots"

View File

@ -130,9 +130,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

@ -1,14 +1,26 @@
import arrow
import pytest
from unittest.mock import patch
from mobilizon_bots.event.event_selector import SelectNextEventStrategy
from mobilizon_bots.config.config import settings
from mobilizon_bots.event.event_selection_strategies import SelectNextEventStrategy
@pytest.fixture
def mock_publication_window(publication_window):
begin, end = publication_window
settings.update({"publishing.window.begin": begin, "publishing.window.end": end})
@pytest.mark.parametrize("current_hour", [10])
@pytest.mark.parametrize(
"desired_break_window_days,days_passed_from_publication", [[2, 1], [3, 2]]
)
def test_window_simple_no_event(
event_generator, desired_break_window_days, days_passed_from_publication
def test_break_window_simple_no_event(
event_generator,
desired_break_window_days,
days_passed_from_publication,
mock_arrow_now,
):
"Testing that the break between events is respected"
unpublished_events = [
@ -32,13 +44,15 @@ def test_window_simple_no_event(
assert selected_event is None
@pytest.mark.parametrize("current_hour", [15])
@pytest.mark.parametrize(
"desired_break_window_days,days_passed_from_publication", [[1, 2], [2, 10], [4, 4]]
)
def test_window_simple_event_found(
def test_break_window_simple_event_found(
event_generator,
desired_break_window_days,
days_passed_from_publication,
mock_arrow_now,
):
"Testing that the break between events is respected and an event is found"
unpublished_events = [
@ -59,16 +73,19 @@ def test_window_simple_event_found(
selected_event = SelectNextEventStrategy(
minimum_break_between_events_in_minutes=desired_break_window_days * 24 * 60
).select(published_events, unpublished_events)
assert selected_event is unpublished_events[0]
@pytest.mark.parametrize("current_hour", [15])
@pytest.mark.parametrize(
"desired_break_window_days,days_passed_from_publication", [[1, 2], [2, 10], [4, 4]]
)
def test_window_multi_event_found(
def test_break_window_multi_event_found(
event_generator,
desired_break_window_days,
days_passed_from_publication,
mock_arrow_now,
):
"Testing that the break between events is respected when there are multiple events"
unpublished_events = [
@ -110,3 +127,56 @@ def test_window_multi_event_found(
minimum_break_between_events_in_minutes=desired_break_window_days * 24 * 60
).select(published_events, unpublished_events)
assert selected_event is unpublished_events[0]
@pytest.fixture
def mock_arrow_now(current_hour):
def mock_now():
return arrow.Arrow(year=2021, month=1, day=1, hour=current_hour)
with patch("arrow.now", mock_now):
yield
@pytest.mark.parametrize("current_hour", [14, 15, 16, 18])
@pytest.mark.parametrize("publication_window", [(14, 19)])
def test_publishing_inner_window_true(mock_arrow_now, mock_publication_window):
"""
Testing that the window check correctly returns True when in an inner publishing window.
"""
assert SelectNextEventStrategy(
minimum_break_between_events_in_minutes=1
).is_in_publishing_window()
@pytest.mark.parametrize("current_hour", [2, 10, 11, 19])
@pytest.mark.parametrize("publication_window", [(14, 19)])
def test_publishing_inner_window_false(mock_arrow_now, mock_publication_window):
"""
Testing that the window check correctly returns False when not in an inner publishing window.
"""
assert not SelectNextEventStrategy(
minimum_break_between_events_in_minutes=1
).is_in_publishing_window()
@pytest.mark.parametrize("current_hour", [2, 10, 11, 19])
@pytest.mark.parametrize("publication_window", [(19, 14)])
def test_publishing_outer_window_true(mock_arrow_now, mock_publication_window):
"""
Testing that the window check correctly returns True when in an outer publishing window.
"""
assert SelectNextEventStrategy(
minimum_break_between_events_in_minutes=1
).is_in_publishing_window()
@pytest.mark.parametrize("current_hour", [14, 15, 16, 18])
@pytest.mark.parametrize("publication_window", [(19, 14)])
def test_publishing_outer_window_false(mock_arrow_now, mock_publication_window):
"""
Testing that the window check correctly returns False when not in an outer publishing window.
"""
assert not SelectNextEventStrategy(
minimum_break_between_events_in_minutes=1
).is_in_publishing_window()

View File

@ -15,10 +15,7 @@ def mock_mobilizon_success_answer(mobilizon_answer, mobilizon_url):
with responses.RequestsMock() as rsps:
rsps.add(
responses.POST,
mobilizon_url,
json=mobilizon_answer,
status=200,
responses.POST, mobilizon_url, json=mobilizon_answer, status=200,
)
yield
@ -29,8 +26,6 @@ def mock_mobilizon_failure_answer(mobilizon_url):
with responses.RequestsMock() as rsps:
rsps.add(
responses.POST,
mobilizon_url,
status=500,
responses.POST, mobilizon_url, status=500,
)
yield

View File

@ -1,7 +1,7 @@
import pytest
import arrow
from mobilizon_bots.event.event import MobilizonEvent
from mobilizon_bots.event.event import PublicationStatus, MobilizonEvent
from mobilizon_bots.mobilizon.events import (
get_mobilizon_future_events,
MobilizonRequestFailed,

View File

@ -19,7 +19,6 @@ def test_event():
"description": "TestDescr",
"begin_datetime": now,
"end_datetime": now + timedelta(hours=1),
"last_accessed": now,
"mobilizon_link": "",
"mobilizon_id": "",
}

View File

@ -13,12 +13,20 @@ group="my_group"
[default.selection]
strategy = "next_event"
[default.publishing.window]
begin=12
end=18
[default.selection.strategy_options]
break_between_events_in_minutes =5
[default.publisher.telegram]
active=true
chat_id="xxx"
msg_template_path="xxx"
token="xxx"
username="xxx"
[default.publisher.facebook]
active=false
[default.publisher.zulip]
@ -31,6 +39,8 @@ active=false
[default.notifier.telegram]
active=true
chat_id="xxx"
token="xxx"
username="xxx"
[default.notifier.zulip]
active=false
[default.notifier.twitter]