From b8525e3be168f223161ff585433104b63594be7a Mon Sep 17 00:00:00 2001 From: Bence Skorka <skorka.bence@gmail.com> Date: Wed, 21 Sep 2022 10:39:13 +0200 Subject: [PATCH 1/7] Add calendarv2 and exclude day of week rule --- notify/calendar/__tests__/test_calendarv2.py | 42 +++++++++++++++++++ notify/calendar/calendarv2.py | 19 +++++++++ .../rules/__tests__/test_exclude_dow.py | 14 +++++++ notify/calendar/rules/exclude_dow.py | 15 +++++++ notify/calendar/rules/loader.py | 3 ++ 5 files changed, 93 insertions(+) create mode 100644 notify/calendar/__tests__/test_calendarv2.py create mode 100644 notify/calendar/calendarv2.py create mode 100644 notify/calendar/rules/__tests__/test_exclude_dow.py create mode 100644 notify/calendar/rules/exclude_dow.py diff --git a/notify/calendar/__tests__/test_calendarv2.py b/notify/calendar/__tests__/test_calendarv2.py new file mode 100644 index 0000000..a283559 --- /dev/null +++ b/notify/calendar/__tests__/test_calendarv2.py @@ -0,0 +1,42 @@ +from pytest import fixture +from datetime import date + +from notify.calendar.calendarv2 import Calendar + + +@fixture +def calendar(): + return Calendar([ + "exclude_all", + "include_weekdays", + { + "name": "exclude_holidays", + "options": { + "country": "HU" + } + }, + { + "name": "exclude_dow", + "options": { + "day": 2 + } + } + ]) + + +def test_weekday_detection(calendar: Calendar): + assert calendar.match_day(date(2022, 7, 22)) is True + assert calendar.match_day(date(2022, 7, 23)) is False + assert calendar.match_day(date(2022, 7, 25)) is True + + +def test_holiday_detection(calendar: Calendar): + # 12.26 is a monday but it is Christmas so it should not be a workday + assert calendar.match_day(date(2022, 12, 26)) is False + assert calendar.match_day(date(2022, 12, 27)) is True + + +def test_midweek(calendar: Calendar): + assert calendar.match_day(date(2022, 9, 13)) is True + assert calendar.match_day(date(2022, 9, 14)) is False + assert calendar.match_day(date(2022, 9, 15)) is True diff --git a/notify/calendar/calendarv2.py b/notify/calendar/calendarv2.py new file mode 100644 index 0000000..b352978 --- /dev/null +++ b/notify/calendar/calendarv2.py @@ -0,0 +1,19 @@ +from datetime import date + +from .rules.rule import CalendarRule +from .rules.loader import load_rules + + +class Calendar: + def __init__(self, rules_config: list): + self._rules: list[CalendarRule] = load_rules(rules_config) + + def match_day(self, qdate: date) -> bool: + included = False + + for rule in self._rules: + evaled = rule.apply(qdate) + if evaled.applicable: + included = evaled.included + + return included diff --git a/notify/calendar/rules/__tests__/test_exclude_dow.py b/notify/calendar/rules/__tests__/test_exclude_dow.py new file mode 100644 index 0000000..fcc05d5 --- /dev/null +++ b/notify/calendar/rules/__tests__/test_exclude_dow.py @@ -0,0 +1,14 @@ +from pytest import fixture +from datetime import date +from notify.calendar.rules.exclude_dow import ExcludeDayOfWeek, RuleResult + + +@fixture +def rule(): + return ExcludeDayOfWeek(2) + + +def test_days(rule: ExcludeDayOfWeek): + assert rule.apply(date(2022, 9, 13)).applicable is False + assert rule.apply(date(2022, 9, 14)) == RuleResult(applicable=True, included=False) + assert rule.apply(date(2022, 9, 15)).applicable is False diff --git a/notify/calendar/rules/exclude_dow.py b/notify/calendar/rules/exclude_dow.py new file mode 100644 index 0000000..ecb576c --- /dev/null +++ b/notify/calendar/rules/exclude_dow.py @@ -0,0 +1,15 @@ +from datetime import date +from .rule import CalendarRule, RuleResult + + +class ExcludeDayOfWeek(CalendarRule): + def __init__(self, day: int): + super().__init__() + + self._exclude_day = day + + def apply(self, day: date) -> RuleResult: + if day.weekday() == self._exclude_day: + return RuleResult(applicable=True, included=False) + else: + return RuleResult(applicable=False, included=False) diff --git a/notify/calendar/rules/loader.py b/notify/calendar/rules/loader.py index 4cbe919..7014cae 100644 --- a/notify/calendar/rules/loader.py +++ b/notify/calendar/rules/loader.py @@ -29,5 +29,8 @@ def load_rule(rule_config: dict) -> CalendarRule: elif rule_config["name"] == "include_days": from .include_days import IncludeDays return IncludeDays(**options) + elif rule_config["name"] == "exclude_dow": + from .exclude_dow import ExcludeDayOfWeek + return ExcludeDayOfWeek(**options) else: raise ValueError("Unknown rule: " + rule_config["name"]) -- GitLab From c06b84e8dfcb63390cba59551e340c053616a91c Mon Sep 17 00:00:00 2001 From: Bence Skorka <skorka.bence@gmail.com> Date: Wed, 21 Sep 2022 11:12:20 +0200 Subject: [PATCH 2/7] Create basic timeline iterator --- notify/timeline/__tests__/test_basic.py | 72 +++++++++++++++++++++++++ notify/timeline/basic.py | 65 ++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 notify/timeline/__tests__/test_basic.py create mode 100644 notify/timeline/basic.py diff --git a/notify/timeline/__tests__/test_basic.py b/notify/timeline/__tests__/test_basic.py new file mode 100644 index 0000000..349dc87 --- /dev/null +++ b/notify/timeline/__tests__/test_basic.py @@ -0,0 +1,72 @@ +from pytest import fixture +from datetime import date + +from notify.timeline.basic import BasicTimelineIterator, FixedDay, CalendarDay +from notify.calendar.calendarv2 import Calendar + + +@fixture +def basic_timeline(): + return BasicTimelineIterator( + workday_calendar=Calendar([ + "exclude_all", + "include_weekdays", + { + "name": "exclude_holidays", + "options": { + "country": "HU" + } + }, + ]), + fixed_day=FixedDay( + day=date(2022, 9, 21), + moderator="a", + days_left=2 + ), + moderator_days=3, + moderators=["a", "b", "c"] + ) + + +def test_fixed_day(basic_timeline: BasicTimelineIterator): + basic_timeline.set_day(date(2022, 9, 21)) + assert basic_timeline.__next__() == CalendarDay(day=date(2022, 9, 21), moderator="a") + + +def test_iteration(basic_timeline: BasicTimelineIterator): + expected_items = [ + CalendarDay(day=date(2022, 9, 21), moderator="a"), + CalendarDay(day=date(2022, 9, 22), moderator="a"), + CalendarDay(day=date(2022, 9, 23), moderator="a"), + CalendarDay(day=date(2022, 9, 26), moderator="b"), + CalendarDay(day=date(2022, 9, 27), moderator="b"), + CalendarDay(day=date(2022, 9, 28), moderator="b"), + CalendarDay(day=date(2022, 9, 29), moderator="c"), + ] + + basic_timeline.set_day(date(2022, 9, 21)) + + for day in basic_timeline: + expected = expected_items.pop(0) + assert day == expected + + if len(expected_items) == 0: + break + + +def test_far_in_future(basic_timeline: BasicTimelineIterator): + expected_items = [ + CalendarDay(day=date(2046, 9, 21), moderator="c"), + CalendarDay(day=date(2046, 9, 24), moderator="c"), + CalendarDay(day=date(2046, 9, 25), moderator="c"), + CalendarDay(day=date(2046, 9, 26), moderator="a"), + ] + + basic_timeline.set_day(date(2046, 9, 21)) + + for day in basic_timeline: + expected = expected_items.pop(0) + assert day == expected + + if len(expected_items) == 0: + break diff --git a/notify/timeline/basic.py b/notify/timeline/basic.py new file mode 100644 index 0000000..54b450b --- /dev/null +++ b/notify/timeline/basic.py @@ -0,0 +1,65 @@ +from dataclasses import dataclass +from datetime import date, timedelta + +from ..calendar.calendarv2 import Calendar + + +@dataclass +class FixedDay(): + day: date + moderator: str + days_left: int + + +@dataclass(frozen=True) +class CalendarDay: + day: date + moderator: str + + +class BasicTimelineIterator(): + def __init__(self, + workday_calendar: Calendar, fixed_day: FixedDay, + moderators: list[str], moderator_days: int) -> None: + self._workday_calendar = workday_calendar + self._fixed_day = fixed_day + self._moderators = moderators + self._moderator_days = moderator_days + + self._offset_days = self._get_initial_offset() + + def __iter__(self) -> 'BasicTimelineIterator': + return self + + def _get_initial_offset(self): + return \ + self._moderators.index(self._fixed_day.moderator) * self._moderator_days + \ + self._moderator_days - self._fixed_day.days_left - 1 + + def _count_days(self, from_date: date, target_date: date) -> int: + from_date = from_date + iterator = from_date + date_counter = 0 + + while iterator < target_date: + iterator += timedelta(days=1) + if self._workday_calendar.match_day(iterator): + date_counter += 1 + + return date_counter + + def set_day(self, day: date) -> None: + self._check_day = day - timedelta(days=1) + + def __next__(self) -> 'BasicTimelineIterator': + while True: + self._check_day += timedelta(days=1) + + if self._workday_calendar.match_day(self._check_day): + count = self._count_days(self._fixed_day.day, self._check_day) + moderator_id = (count + self._offset_days) // self._moderator_days + + return CalendarDay( + day=self._check_day, + moderator=self._moderators[moderator_id % len(self._moderators)] + ) -- GitLab From 0f001edd4b9faac77ac9e4b6112c4a86baf7b77a Mon Sep 17 00:00:00 2001 From: Bence Skorka <skorka.bence@gmail.com> Date: Sat, 24 Sep 2022 16:27:52 +0200 Subject: [PATCH 3/7] Add new message generator --- notify/channels/vonage_sms.py | 3 +- notify/messages/__tests__/test_generatorv2.py | 70 +++++++++++++++++++ notify/messages/generator.py | 3 +- notify/messages/generatorv2.py | 42 +++++++++++ notify/messages/notification.py | 2 + notify/timeline/basic.py | 18 ++--- notify/timeline/iterator.py | 16 +++++ requirements.txt | 1 + templates/daily-short.j2 | 5 ++ templates/daily.j2 | 10 +++ 10 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 notify/messages/__tests__/test_generatorv2.py create mode 100644 notify/messages/generatorv2.py create mode 100644 notify/timeline/iterator.py create mode 100644 templates/daily-short.j2 create mode 100644 templates/daily.j2 diff --git a/notify/channels/vonage_sms.py b/notify/channels/vonage_sms.py index 834d32f..b970e3a 100644 --- a/notify/channels/vonage_sms.py +++ b/notify/channels/vonage_sms.py @@ -18,7 +18,8 @@ class VonageSmsChannel(NotificationChannel): self._client.send_message({ 'from': self._sender, 'to': number, - 'text': message.message + 'text': message.short_message + if message.short_message else message.message }) except Exception as e: print(f"Failed to send SMS: {e}") diff --git a/notify/messages/__tests__/test_generatorv2.py b/notify/messages/__tests__/test_generatorv2.py new file mode 100644 index 0000000..bc2fda3 --- /dev/null +++ b/notify/messages/__tests__/test_generatorv2.py @@ -0,0 +1,70 @@ +from pytest import fixture +from datetime import date + +from notify.timeline.iterator import TimelineIterator +from notify.timeline.basic import BasicTimelineIterator, FixedDay +from notify.calendar.calendarv2 import Calendar +from notify.messages.generatorv2 import build_notification + + +@fixture +def basic_timeline_iterator(): + return BasicTimelineIterator( + workday_calendar=Calendar([ + "exclude_all", + "include_weekdays", + { + "name": "exclude_holidays", + "options": { + "country": "HU" + } + }, + ]), + fixed_day=FixedDay( + day=date(2022, 9, 21), + moderator="John", + days_left=2 + ), + moderator_days=3, + moderators=["John", "Jane", "Jack"] + ) + + +@fixture +def template_config(): + return { + "message": { + "title": "Daily standup", + "normal_template": "daily.j2", + "short_template": "daily-short.j2" + } + } + + +def test_message_generation(basic_timeline_iterator: TimelineIterator, template_config): + basic_timeline_iterator.set_day(date(2022, 9, 21)) + msg = build_notification(basic_timeline_iterator, template_config) + assert msg.title == "Daily standup" + assert msg.message == """Dear Team Tirith! + +Today John will hold the daily meeting! + +The upcoming schedule for the next 10 workdays is as follows: + 2022-09-22 (Thursday): John + 2022-09-23 (Friday): John + 2022-09-26 (Monday): Jane + 2022-09-27 (Tuesday): Jane + 2022-09-28 (Wednesday): Jane + 2022-09-29 (Thursday): Jack + 2022-09-30 (Friday): Jack + 2022-10-03 (Monday): Jack + 2022-10-04 (Tuesday): John + 2022-10-05 (Wednesday): John + +Have a nice day!""" + + assert msg.short_message == """Dear Team Tirith! + +Today John will hold the daily meeting! + +Have a nice day!""" diff --git a/notify/messages/generator.py b/notify/messages/generator.py index 72da79e..f6b6949 100644 --- a/notify/messages/generator.py +++ b/notify/messages/generator.py @@ -6,5 +6,6 @@ def generate_notification(config, info: DailyStandupInfo) -> Notification: template = config["message"]["message_template"] return Notification( title=config["message"]["title"], - message=template.format(**info.__dict__) + message=template.format(**info.__dict__), + short_message=template.format(**info.__dict__) ) diff --git a/notify/messages/generatorv2.py b/notify/messages/generatorv2.py new file mode 100644 index 0000000..42c4609 --- /dev/null +++ b/notify/messages/generatorv2.py @@ -0,0 +1,42 @@ +import jinja2 + +from ..timeline.iterator import TimelineIterator, CalendarDay +from .notification import Notification + + +def render(tempalte_file, data) -> str: + templateLoader = jinja2.FileSystemLoader(searchpath="./templates") + templateEnv = jinja2.Environment(loader=templateLoader) + template = templateEnv.get_template(tempalte_file) + return template.render(data) + + +def day_to_data(day: CalendarDay) -> dict: + return { + **day.__dict__, + "dow": day.day.strftime('%A') + } + + +def build_notification(iter: TimelineIterator, config) -> Notification: + days: list[CalendarDay] = [] + for day in iter: + days.append(day) + if len(days) >= 11: + break + + template_data = { + "today": day_to_data(days[0]) if len(days) >= 1 else None, + "days": [day_to_data(day) for day in days[1:]] + } + + title = config["message"]["title"] + normal_msg_template_file = config["message"]["normal_template"] + short_msg_template_file = config["message"]["short_template"] + + return Notification( + title=title, + message=render(normal_msg_template_file, template_data), + short_message=render(short_msg_template_file, template_data) + if short_msg_template_file else None + ) diff --git a/notify/messages/notification.py b/notify/messages/notification.py index 3ece9f8..802578b 100644 --- a/notify/messages/notification.py +++ b/notify/messages/notification.py @@ -1,7 +1,9 @@ from dataclasses import dataclass +from typing import Optional @dataclass(frozen=True) class Notification: title: str message: str + short_message: Optional[str] diff --git a/notify/timeline/basic.py b/notify/timeline/basic.py index 54b450b..fe05498 100644 --- a/notify/timeline/basic.py +++ b/notify/timeline/basic.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from datetime import date, timedelta +from .iterator import TimelineIterator, CalendarDay from ..calendar.calendarv2 import Calendar @@ -11,13 +12,7 @@ class FixedDay(): days_left: int -@dataclass(frozen=True) -class CalendarDay: - day: date - moderator: str - - -class BasicTimelineIterator(): +class BasicTimelineIterator(TimelineIterator): def __init__(self, workday_calendar: Calendar, fixed_day: FixedDay, moderators: list[str], moderator_days: int) -> None: @@ -28,9 +23,6 @@ class BasicTimelineIterator(): self._offset_days = self._get_initial_offset() - def __iter__(self) -> 'BasicTimelineIterator': - return self - def _get_initial_offset(self): return \ self._moderators.index(self._fixed_day.moderator) * self._moderator_days + \ @@ -50,11 +42,15 @@ class BasicTimelineIterator(): def set_day(self, day: date) -> None: self._check_day = day - timedelta(days=1) + self._quit_day = day + timedelta(days=365) - def __next__(self) -> 'BasicTimelineIterator': + def __next__(self) -> CalendarDay: while True: self._check_day += timedelta(days=1) + if self._check_day > self._quit_day: + raise StopIteration + if self._workday_calendar.match_day(self._check_day): count = self._count_days(self._fixed_day.day, self._check_day) moderator_id = (count + self._offset_days) // self._moderator_days diff --git a/notify/timeline/iterator.py b/notify/timeline/iterator.py new file mode 100644 index 0000000..a6c414f --- /dev/null +++ b/notify/timeline/iterator.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from datetime import date + + +@dataclass(frozen=True) +class CalendarDay: + day: date + moderator: str + + +class TimelineIterator(): + def __iter__(self) -> 'TimelineIterator': + return self + + def __next__(self) -> 'CalendarDay': + raise NotImplementedError diff --git a/requirements.txt b/requirements.txt index f10e509..eeed271 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ nexmo redmail holidays deepmerge +jinja2 # test pytest diff --git a/templates/daily-short.j2 b/templates/daily-short.j2 new file mode 100644 index 0000000..4ea61d6 --- /dev/null +++ b/templates/daily-short.j2 @@ -0,0 +1,5 @@ +Dear Team Tirith! + +Today {{ today.moderator }} will hold the daily meeting! + +Have a nice day! diff --git a/templates/daily.j2 b/templates/daily.j2 new file mode 100644 index 0000000..e47e0ca --- /dev/null +++ b/templates/daily.j2 @@ -0,0 +1,10 @@ +Dear Team Tirith! + +Today {{ today.moderator }} will hold the daily meeting! + +The upcoming schedule for the next {{ days | length }} workdays is as follows: +{%- for item in days %} + {{ item.day }} ({{ item.dow }}): {{ item.moderator }} +{%- endfor %} + +Have a nice day! -- GitLab From a22f479233f689053600a109af3e981033818534 Mon Sep 17 00:00:00 2001 From: Bence Skorka <skorka.bence@gmail.com> Date: Sun, 25 Sep 2022 17:34:59 +0200 Subject: [PATCH 4/7] Use new timeline generators --- main.py | 39 +++++++++++++------ notify/messages/__tests__/test_generatorv2.py | 8 +++- notify/messages/generatorv2.py | 8 +++- notify/timeline/iterator.py | 3 ++ 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/main.py b/main.py index 36c35a1..1dd4ccd 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,11 @@ import argparse import os +from datetime import date + from notify.config import load_config, build_channels -from notify.calendar.calendar import Calendar -from notify.messages.generator import generate_notification +from notify.timeline.basic import BasicTimelineIterator, FixedDay +from notify.messages.generatorv2 import build_notification +from notify.calendar.calendarv2 import Calendar def parse_args(): @@ -19,28 +22,40 @@ def is_real_run(args): or os.environ.get("DO_REAL_RUN", "false") == "true" +def get_date(date_str: str) -> date: + if date_str == "today": + return date.today() + else: + return date.fromisoformat(date_str) + + def main(): args = parse_args() config = load_config(args.config) channels = list(build_channels(config)) - calendar = Calendar(config) - info = calendar.get_daily_standup_info(args.day) - - if not info: - print("No standup today") + timeline = BasicTimelineIterator( + workday_calendar=Calendar(config["calendar"]["rules"]), + fixed_day=FixedDay( + day=config["calendar"]["fixed_point"]["date"], + moderator=config["calendar"]["fixed_point"]["moderator"], + days_left=config["calendar"]["fixed_point"]["days_left"] + ), + moderator_days=config["calendar"]["moderator_days"], + moderators=config["moderators"] + ) + notification = build_notification(timeline, config, get_date(args.day)) + if not notification: + print("No notification to send") else: - message = generate_notification(config, info) - if is_real_run(args): print("Sending messages on channels...") for channel in channels: - channel.send_notification(message) + channel.send_notification(notification) else: print("--- This is a dry-run, messages are not sent ---") - print(message.message) - + print(notification.message) if __name__ == "__main__": main() diff --git a/notify/messages/__tests__/test_generatorv2.py b/notify/messages/__tests__/test_generatorv2.py index bc2fda3..37e837e 100644 --- a/notify/messages/__tests__/test_generatorv2.py +++ b/notify/messages/__tests__/test_generatorv2.py @@ -41,9 +41,13 @@ def template_config(): } +def test_weekend(basic_timeline_iterator: TimelineIterator, template_config): + msg = build_notification(basic_timeline_iterator, template_config, date(2022, 9, 25)) + assert msg is None + + def test_message_generation(basic_timeline_iterator: TimelineIterator, template_config): - basic_timeline_iterator.set_day(date(2022, 9, 21)) - msg = build_notification(basic_timeline_iterator, template_config) + msg = build_notification(basic_timeline_iterator, template_config, date(2022, 9, 21)) assert msg.title == "Daily standup" assert msg.message == """Dear Team Tirith! diff --git a/notify/messages/generatorv2.py b/notify/messages/generatorv2.py index 42c4609..eae6588 100644 --- a/notify/messages/generatorv2.py +++ b/notify/messages/generatorv2.py @@ -1,4 +1,5 @@ import jinja2 +from datetime import date from ..timeline.iterator import TimelineIterator, CalendarDay from .notification import Notification @@ -18,13 +19,18 @@ def day_to_data(day: CalendarDay) -> dict: } -def build_notification(iter: TimelineIterator, config) -> Notification: +def build_notification(iter: TimelineIterator, config, starting_day: date) -> Notification: + iter.set_day(starting_day) + days: list[CalendarDay] = [] for day in iter: days.append(day) if len(days) >= 11: break + if len(days) == 0 or days[0].day != starting_day: + return None + template_data = { "today": day_to_data(days[0]) if len(days) >= 1 else None, "days": [day_to_data(day) for day in days[1:]] diff --git a/notify/timeline/iterator.py b/notify/timeline/iterator.py index a6c414f..5cdb9f0 100644 --- a/notify/timeline/iterator.py +++ b/notify/timeline/iterator.py @@ -12,5 +12,8 @@ class TimelineIterator(): def __iter__(self) -> 'TimelineIterator': return self + def set_day(self, day: date) -> None: + raise NotImplementedError + def __next__(self) -> 'CalendarDay': raise NotImplementedError -- GitLab From 8f8cf1dedaaa458b6508626e3c821bb2210167fb Mon Sep 17 00:00:00 2001 From: Bence Skorka <skorka.bence@gmail.com> Date: Sun, 25 Sep 2022 17:37:10 +0200 Subject: [PATCH 5/7] Remove old calendar and message generator --- main.py | 4 +- .../{test_calendarv2.py => test_calendar.py} | 2 +- notify/calendar/calendar.py | 97 +------------------ notify/calendar/calendarv2.py | 19 ---- ...{test_generatorv2.py => test_generator.py} | 4 +- notify/messages/generator.py | 49 ++++++++-- notify/messages/generatorv2.py | 48 --------- notify/timeline/__tests__/test_basic.py | 2 +- notify/timeline/basic.py | 2 +- 9 files changed, 54 insertions(+), 173 deletions(-) rename notify/calendar/__tests__/{test_calendarv2.py => test_calendar.py} (95%) delete mode 100644 notify/calendar/calendarv2.py rename notify/messages/__tests__/{test_generatorv2.py => test_generator.py} (94%) delete mode 100644 notify/messages/generatorv2.py diff --git a/main.py b/main.py index 1dd4ccd..32bfd51 100644 --- a/main.py +++ b/main.py @@ -4,8 +4,8 @@ from datetime import date from notify.config import load_config, build_channels from notify.timeline.basic import BasicTimelineIterator, FixedDay -from notify.messages.generatorv2 import build_notification -from notify.calendar.calendarv2 import Calendar +from notify.messages.generator import build_notification +from notify.calendar.calendar import Calendar def parse_args(): diff --git a/notify/calendar/__tests__/test_calendarv2.py b/notify/calendar/__tests__/test_calendar.py similarity index 95% rename from notify/calendar/__tests__/test_calendarv2.py rename to notify/calendar/__tests__/test_calendar.py index a283559..20d803c 100644 --- a/notify/calendar/__tests__/test_calendarv2.py +++ b/notify/calendar/__tests__/test_calendar.py @@ -1,7 +1,7 @@ from pytest import fixture from datetime import date -from notify.calendar.calendarv2 import Calendar +from notify.calendar.calendar import Calendar @fixture diff --git a/notify/calendar/calendar.py b/notify/calendar/calendar.py index 830d240..b352978 100644 --- a/notify/calendar/calendar.py +++ b/notify/calendar/calendar.py @@ -1,41 +1,14 @@ -from dataclasses import dataclass -from typing import Optional -from datetime import date, timedelta +from datetime import date from .rules.rule import CalendarRule from .rules.loader import load_rules -accepted_weekdays = [0, 1, 2, 3, 4] - - -@dataclass(frozen=True) -class CalendarDay: - day: date - moderator: str - - -@dataclass(frozen=True) -class DailyStandupInfo: - moderator_name: str - days_left: int - next_moderator_name: str - class Calendar: - def __init__(self, config): - self._moderators = config["moderators"] - self._moderator_days = config["calendar"]["moderator_days"] + def __init__(self, rules_config: list): + self._rules: list[CalendarRule] = load_rules(rules_config) - self._fixed_date: date = config["calendar"]["fixed_point"]["date"] - self._fixed_moderator: str = config["calendar"]["fixed_point"]["moderator"] - self._fixed_moderator_days: str = config["calendar"]["fixed_point"]["days_left"] - - if self._fixed_moderator not in self._moderators: - raise ValueError("Fixed moderator is not in the list of moderators") - - self._rules: list[CalendarRule] = load_rules(config["calendar"]["rules"]) - - def _is_workday(self, qdate: date) -> bool: + def match_day(self, qdate: date) -> bool: included = False for rule in self._rules: @@ -44,65 +17,3 @@ class Calendar: included = evaled.included return included - - def _get_moderator_offset(self): - return \ - self._moderators.index(self._fixed_moderator) * self._moderator_days + \ - self._moderator_days - self._fixed_moderator_days - 1 - - def _count_days(self, from_date: date, target_date: date) -> int: - from_date = from_date - iterator = from_date - date_counter = 0 - - while iterator < target_date: - iterator += timedelta(days=1) - if self._is_workday(iterator): - date_counter += 1 - - return date_counter - - def _build_calendar(self, from_day: date) -> list[CalendarDay]: - days: list[CalendarDay] = [] - last_cached_day = self._fixed_date - last_cached_day_count = 0 - moderator_offset = self._get_moderator_offset() - - check_day = from_day - while len(days) < (self._moderator_days + 2): - if self._is_workday(check_day): - count = self._count_days(last_cached_day, check_day) + last_cached_day_count - last_cached_day = check_day - last_cached_day_count = count - - moderator_id = (count + moderator_offset) // self._moderator_days - days.append(CalendarDay( - day=check_day, - moderator=self._moderators[moderator_id % len(self._moderators)] - )) - check_day += timedelta(days=1) - - return days - - def get_daily_standup_info(self, day: str) -> Optional[DailyStandupInfo]: - if day == "today": - today = date.today() - else: - today = date.fromisoformat(day) - - if not self._is_workday(today): - return None - days = self._build_calendar(today) - - current_moderator = days[0].moderator - days_left = 0 - i = 0 - while current_moderator == days[i+1].moderator: - days_left += 1 - i += 1 - - return DailyStandupInfo( - moderator_name=current_moderator, - days_left=days_left, - next_moderator_name=days[i+1].moderator - ) diff --git a/notify/calendar/calendarv2.py b/notify/calendar/calendarv2.py deleted file mode 100644 index b352978..0000000 --- a/notify/calendar/calendarv2.py +++ /dev/null @@ -1,19 +0,0 @@ -from datetime import date - -from .rules.rule import CalendarRule -from .rules.loader import load_rules - - -class Calendar: - def __init__(self, rules_config: list): - self._rules: list[CalendarRule] = load_rules(rules_config) - - def match_day(self, qdate: date) -> bool: - included = False - - for rule in self._rules: - evaled = rule.apply(qdate) - if evaled.applicable: - included = evaled.included - - return included diff --git a/notify/messages/__tests__/test_generatorv2.py b/notify/messages/__tests__/test_generator.py similarity index 94% rename from notify/messages/__tests__/test_generatorv2.py rename to notify/messages/__tests__/test_generator.py index 37e837e..3f20db2 100644 --- a/notify/messages/__tests__/test_generatorv2.py +++ b/notify/messages/__tests__/test_generator.py @@ -3,8 +3,8 @@ from datetime import date from notify.timeline.iterator import TimelineIterator from notify.timeline.basic import BasicTimelineIterator, FixedDay -from notify.calendar.calendarv2 import Calendar -from notify.messages.generatorv2 import build_notification +from notify.calendar.calendar import Calendar +from notify.messages.generator import build_notification @fixture diff --git a/notify/messages/generator.py b/notify/messages/generator.py index f6b6949..eae6588 100644 --- a/notify/messages/generator.py +++ b/notify/messages/generator.py @@ -1,11 +1,48 @@ -from ..calendar.calendar import DailyStandupInfo +import jinja2 +from datetime import date + +from ..timeline.iterator import TimelineIterator, CalendarDay from .notification import Notification -def generate_notification(config, info: DailyStandupInfo) -> Notification: - template = config["message"]["message_template"] +def render(tempalte_file, data) -> str: + templateLoader = jinja2.FileSystemLoader(searchpath="./templates") + templateEnv = jinja2.Environment(loader=templateLoader) + template = templateEnv.get_template(tempalte_file) + return template.render(data) + + +def day_to_data(day: CalendarDay) -> dict: + return { + **day.__dict__, + "dow": day.day.strftime('%A') + } + + +def build_notification(iter: TimelineIterator, config, starting_day: date) -> Notification: + iter.set_day(starting_day) + + days: list[CalendarDay] = [] + for day in iter: + days.append(day) + if len(days) >= 11: + break + + if len(days) == 0 or days[0].day != starting_day: + return None + + template_data = { + "today": day_to_data(days[0]) if len(days) >= 1 else None, + "days": [day_to_data(day) for day in days[1:]] + } + + title = config["message"]["title"] + normal_msg_template_file = config["message"]["normal_template"] + short_msg_template_file = config["message"]["short_template"] + return Notification( - title=config["message"]["title"], - message=template.format(**info.__dict__), - short_message=template.format(**info.__dict__) + title=title, + message=render(normal_msg_template_file, template_data), + short_message=render(short_msg_template_file, template_data) + if short_msg_template_file else None ) diff --git a/notify/messages/generatorv2.py b/notify/messages/generatorv2.py deleted file mode 100644 index eae6588..0000000 --- a/notify/messages/generatorv2.py +++ /dev/null @@ -1,48 +0,0 @@ -import jinja2 -from datetime import date - -from ..timeline.iterator import TimelineIterator, CalendarDay -from .notification import Notification - - -def render(tempalte_file, data) -> str: - templateLoader = jinja2.FileSystemLoader(searchpath="./templates") - templateEnv = jinja2.Environment(loader=templateLoader) - template = templateEnv.get_template(tempalte_file) - return template.render(data) - - -def day_to_data(day: CalendarDay) -> dict: - return { - **day.__dict__, - "dow": day.day.strftime('%A') - } - - -def build_notification(iter: TimelineIterator, config, starting_day: date) -> Notification: - iter.set_day(starting_day) - - days: list[CalendarDay] = [] - for day in iter: - days.append(day) - if len(days) >= 11: - break - - if len(days) == 0 or days[0].day != starting_day: - return None - - template_data = { - "today": day_to_data(days[0]) if len(days) >= 1 else None, - "days": [day_to_data(day) for day in days[1:]] - } - - title = config["message"]["title"] - normal_msg_template_file = config["message"]["normal_template"] - short_msg_template_file = config["message"]["short_template"] - - return Notification( - title=title, - message=render(normal_msg_template_file, template_data), - short_message=render(short_msg_template_file, template_data) - if short_msg_template_file else None - ) diff --git a/notify/timeline/__tests__/test_basic.py b/notify/timeline/__tests__/test_basic.py index 349dc87..6299dc6 100644 --- a/notify/timeline/__tests__/test_basic.py +++ b/notify/timeline/__tests__/test_basic.py @@ -2,7 +2,7 @@ from pytest import fixture from datetime import date from notify.timeline.basic import BasicTimelineIterator, FixedDay, CalendarDay -from notify.calendar.calendarv2 import Calendar +from notify.calendar.calendar import Calendar @fixture diff --git a/notify/timeline/basic.py b/notify/timeline/basic.py index fe05498..164b5c6 100644 --- a/notify/timeline/basic.py +++ b/notify/timeline/basic.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from datetime import date, timedelta from .iterator import TimelineIterator, CalendarDay -from ..calendar.calendarv2 import Calendar +from ..calendar.calendar import Calendar @dataclass -- GitLab From 38fecd0442b803a757d0aabb124d5b85719869c9 Mon Sep 17 00:00:00 2001 From: Bence Skorka <skorka.bence@gmail.com> Date: Sun, 25 Sep 2022 17:38:24 +0200 Subject: [PATCH 6/7] Fix formatting --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index 32bfd51..3fbe627 100644 --- a/main.py +++ b/main.py @@ -57,5 +57,6 @@ def main(): print("--- This is a dry-run, messages are not sent ---") print(notification.message) + if __name__ == "__main__": main() -- GitLab From 4628807b4624b5d609834f312ef74a3757e446d3 Mon Sep 17 00:00:00 2001 From: Bence Skorka <skorka.bence@gmail.com> Date: Sun, 25 Sep 2022 17:40:21 +0200 Subject: [PATCH 7/7] Remove unittest of old code --- .../calendar/__tests__/test_usual_calendar.py | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 notify/calendar/__tests__/test_usual_calendar.py diff --git a/notify/calendar/__tests__/test_usual_calendar.py b/notify/calendar/__tests__/test_usual_calendar.py deleted file mode 100644 index ed55ab7..0000000 --- a/notify/calendar/__tests__/test_usual_calendar.py +++ /dev/null @@ -1,41 +0,0 @@ -from pytest import fixture -from datetime import datetime - -from notify.calendar.calendar import Calendar - - -@fixture -def calendar(): - return Calendar({ - "moderators": ["a", "b", "c"], - "calendar": { - "rules": [ - "exclude_all", - "include_weekdays", - { - "name": "exclude_holidays", - "options": { - "country": "HU" - } - } - ], - "moderator_days": 3, - "fixed_point": { - "date": datetime(2022, 7, 20), - "moderator": "b", - "days_left": 2 - } - } - }) - - -def test_weekday_detection(calendar: Calendar): - assert calendar._is_workday(datetime(2022, 7, 22)) is True - assert calendar._is_workday(datetime(2022, 7, 23)) is False - assert calendar._is_workday(datetime(2022, 7, 25)) is True - - -def test_holiday_detection(calendar: Calendar): - # 12.26 is a monday but it is Christmas so it should not be a workday - assert calendar._is_workday(datetime(2022, 12, 26)) is False - assert calendar._is_workday(datetime(2022, 12, 27)) is True -- GitLab