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:
parent
6cea51bcab
commit
40919a994f
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
|
|
|
@ -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)
|
|
@ -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:"
|
|
@ -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"},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": "",
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue