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