diff --git a/.gitignore b/.gitignore index 8dd78ba1d24d2c0728739f77a50575dab3379bd2..81a4fdc1696316a178d80ecaa4bde72fa9a57fc6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ ci.config.yaml channels.config.yaml .pytest_cache configs +demo.sh diff --git a/ci/gitlab/tests/lint.yaml b/ci/gitlab/tests/lint.yaml index 130c3a7e2ed4f02f2b378d3e6e3dec266d3ae203..744667a23413f8310e5474e8ce22bbd39cebdbb1 100644 --- a/ci/gitlab/tests/lint.yaml +++ b/ci/gitlab/tests/lint.yaml @@ -1,5 +1,6 @@ test-linting: stage: test script: - - python3 -m flake8 --config flake8.conf ./main.py ./ci + - python3 -m flake8 --config flake8.conf ./main.py ./notify + - python3 -m mypy --follow-imports=silent --ignore-missing-imports --show-column-numbers --strict ./main.py ./notify retry: 2 diff --git a/main.py b/main.py index 87adbe795165befb9a4658d64e87e8a37829a561..c1188dcb0d5f0b3e87aaef88afaf95349f4f0f62 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import argparse import os from datetime import date +from typing import Any from notify.config import load_config, build_channels from notify.timeline.basic import BasicTimelineIterator, FixedDay @@ -8,7 +9,7 @@ from notify.messages.generator import build_notification from notify.calendar.calendar import Calendar -def parse_args(): +def parse_args() -> Any: parser = argparse.ArgumentParser("Daily notifications") parser.add_argument("--config", help="Path to config file", default=[], required=True, action="append") @@ -17,12 +18,12 @@ def parse_args(): return parser.parse_args() -def is_real_run(args): +def is_real_run(args: Any) -> bool: return args.live_run is True \ or os.environ.get("DO_REAL_RUN", "false") == "true" -def get_date(args) -> date: +def get_date(args: Any) -> date: day_var = os.environ.get("OPT_OVERRIDE_DAY") if day_var: return date.fromisoformat(day_var) @@ -32,7 +33,7 @@ def get_date(args) -> date: return date.fromisoformat(args.day) -def main(): +def main() -> None: args = parse_args() config = load_config(args.config) diff --git a/notify/__init__.py b/notify/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notify/calendar/__init__.py b/notify/calendar/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notify/calendar/__tests__/test_calendar.py b/notify/calendar/__tests__/test_calendar.py index 20d803c3bf51052305fa986fde0f44b0fde47d3c..fb0d613775db08bc6ac98fd7c1daaef0459e22f6 100644 --- a/notify/calendar/__tests__/test_calendar.py +++ b/notify/calendar/__tests__/test_calendar.py @@ -5,7 +5,7 @@ from notify.calendar.calendar import Calendar @fixture -def calendar(): +def calendar() -> Calendar: return Calendar([ "exclude_all", "include_weekdays", @@ -24,19 +24,19 @@ def calendar(): ]) -def test_weekday_detection(calendar: Calendar): +def test_weekday_detection(calendar: Calendar) -> None: 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): +def test_holiday_detection(calendar: Calendar) -> None: # 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): +def test_midweek(calendar: Calendar) -> None: 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/calendar.py b/notify/calendar/calendar.py index b352978326d01c06b79414c358d7c7d6a05bd872..9a529d63134ca4902a585ad5f35724bb3177488b 100644 --- a/notify/calendar/calendar.py +++ b/notify/calendar/calendar.py @@ -1,11 +1,12 @@ from datetime import date +from typing import Any from .rules.rule import CalendarRule from .rules.loader import load_rules class Calendar: - def __init__(self, rules_config: list): + def __init__(self, rules_config: list[Any]) -> None: self._rules: list[CalendarRule] = load_rules(rules_config) def match_day(self, qdate: date) -> bool: diff --git a/notify/calendar/rules/__init__.py b/notify/calendar/rules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notify/calendar/rules/__tests__/test_exclude_all.py b/notify/calendar/rules/__tests__/test_exclude_all.py index 367dd6dc130c0a40bee3047c639e4d4df7a17841..518df4a1abe2dd1eedc915247b0521e81d312818 100644 --- a/notify/calendar/rules/__tests__/test_exclude_all.py +++ b/notify/calendar/rules/__tests__/test_exclude_all.py @@ -1,7 +1,8 @@ from datetime import date -from notify.calendar.rules.exclude_all import ExcludeAll, RuleResult +from notify.calendar.rules.exclude_all import ExcludeAll +from notify.calendar.rules.rule import RuleResult -def test_current_date(): +def test_current_date() -> None: rule = ExcludeAll() assert rule.apply(date.today()) == RuleResult(applicable=True, included=False) diff --git a/notify/calendar/rules/__tests__/test_exclude_dow.py b/notify/calendar/rules/__tests__/test_exclude_dow.py index fcc05d5811ef3a18b02f20a09295163277ddbae1..7b76da8c50559cce4e73eb632562e3792bdb3de1 100644 --- a/notify/calendar/rules/__tests__/test_exclude_dow.py +++ b/notify/calendar/rules/__tests__/test_exclude_dow.py @@ -1,14 +1,15 @@ from pytest import fixture from datetime import date -from notify.calendar.rules.exclude_dow import ExcludeDayOfWeek, RuleResult +from notify.calendar.rules.exclude_dow import ExcludeDayOfWeek +from notify.calendar.rules.rule import RuleResult @fixture -def rule(): +def rule() -> ExcludeDayOfWeek: return ExcludeDayOfWeek(2) -def test_days(rule: ExcludeDayOfWeek): +def test_days(rule: ExcludeDayOfWeek) -> None: 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/__tests__/test_exclude_holidays.py b/notify/calendar/rules/__tests__/test_exclude_holidays.py index be321cc5e8b226358bea31599e87324213138f34..6d4794fdbd2de48d4da0497d7fb814df68daff96 100644 --- a/notify/calendar/rules/__tests__/test_exclude_holidays.py +++ b/notify/calendar/rules/__tests__/test_exclude_holidays.py @@ -1,14 +1,15 @@ from pytest import fixture from datetime import date -from notify.calendar.rules.exclude_holidays import ExcludeHolidays, RuleResult +from notify.calendar.rules.exclude_holidays import ExcludeHolidays +from notify.calendar.rules.rule import RuleResult @fixture -def rule(): +def rule() -> ExcludeHolidays: return ExcludeHolidays("HU") -def test_christmas(rule: ExcludeHolidays): +def test_christmas(rule: ExcludeHolidays) -> None: assert rule.apply(date(2021, 12, 23)).applicable is False assert rule.apply(date(2021, 12, 24)) == RuleResult(applicable=True, included=False) assert rule.apply(date(2021, 12, 25)) == RuleResult(applicable=True, included=False) diff --git a/notify/calendar/rules/__tests__/test_include_days.py b/notify/calendar/rules/__tests__/test_include_days.py index 56e049642506e5f25bb8a3afd665448d2b63d197..84a017709f13557aa81350f010efb2ab006bcc28 100644 --- a/notify/calendar/rules/__tests__/test_include_days.py +++ b/notify/calendar/rules/__tests__/test_include_days.py @@ -1,13 +1,14 @@ from datetime import date -from notify.calendar.rules.include_days import IncludeDays, RuleResult +from notify.calendar.rules.include_days import IncludeDays +from notify.calendar.rules.rule import RuleResult -def test_no_included_days(): +def test_no_included_days() -> None: rule = IncludeDays(days=[]) assert rule.apply(date(2022, 9, 18)).applicable is False -def test_one_included_day(): +def test_one_included_day() -> None: rule = IncludeDays(days=[date(2022, 9, 18)]) assert rule.apply(date(2022, 9, 17)).applicable is False @@ -15,7 +16,7 @@ def test_one_included_day(): assert rule.apply(date(2022, 9, 19)).applicable is False -def test_two_included_day(): +def test_two_included_day() -> None: rule = IncludeDays(days=[date(2022, 9, 19), date(2022, 9, 18)]) assert rule.apply(date(2022, 9, 17)).applicable is False diff --git a/notify/calendar/rules/__tests__/test_include_weekdays.py b/notify/calendar/rules/__tests__/test_include_weekdays.py index 70dad37c9d2207d5f6014d52d9242c2858ae24fc..03ee9367fd6438158b6291806213d5f9207e7bc7 100644 --- a/notify/calendar/rules/__tests__/test_include_weekdays.py +++ b/notify/calendar/rules/__tests__/test_include_weekdays.py @@ -1,14 +1,15 @@ from pytest import fixture from datetime import date -from notify.calendar.rules.include_weekdays import IncludeWeekdays, RuleResult +from notify.calendar.rules.include_weekdays import IncludeWeekdays +from notify.calendar.rules.rule import RuleResult @fixture -def rule(): +def rule() -> IncludeWeekdays: return IncludeWeekdays() -def test_weekdays(rule: IncludeWeekdays): +def test_weekdays(rule: IncludeWeekdays) -> None: assert rule.apply(date(2022, 9, 19)) == RuleResult(applicable=True, included=True) assert rule.apply(date(2022, 9, 20)) == RuleResult(applicable=True, included=True) assert rule.apply(date(2022, 9, 21)) == RuleResult(applicable=True, included=True) @@ -16,6 +17,6 @@ def test_weekdays(rule: IncludeWeekdays): assert rule.apply(date(2022, 9, 23)) == RuleResult(applicable=True, included=True) -def test_weekend(rule: IncludeWeekdays): +def test_weekend(rule: IncludeWeekdays) -> None: assert rule.apply(date(2022, 9, 24)).applicable is False assert rule.apply(date(2022, 9, 25)).applicable is False diff --git a/notify/calendar/rules/exclude_holidays.py b/notify/calendar/rules/exclude_holidays.py index fbc24e78a8e2dfbdf3c48edeba8f784f3072334c..dfa4229d96262e63e62ef96c15cd66d47b80418a 100644 --- a/notify/calendar/rules/exclude_holidays.py +++ b/notify/calendar/rules/exclude_holidays.py @@ -4,9 +4,9 @@ from .rule import CalendarRule, RuleResult class ExcludeHolidays(CalendarRule): - def __init__(self, country: str): + def __init__(self, country: str) -> None: super().__init__() - self._holidays = pyholidays.country_holidays(country) + self._holidays = pyholidays.country_holidays(country) # type: ignore def apply(self, day: date) -> RuleResult: if day in self._holidays: diff --git a/notify/calendar/rules/loader.py b/notify/calendar/rules/loader.py index 7014caeeae5573274ea378717f52ae4d635cab17..394c383a8082c443adeb5f468089c04014075ecb 100644 --- a/notify/calendar/rules/loader.py +++ b/notify/calendar/rules/loader.py @@ -1,7 +1,8 @@ +from typing import Any from .rule import CalendarRule -def load_rules(rule_config_list: list) -> list[CalendarRule]: +def load_rules(rule_config_list: list[Any]) -> list[CalendarRule]: rules: list[CalendarRule] = [] for rule_config in rule_config_list: @@ -14,7 +15,7 @@ def load_rules(rule_config_list: list) -> list[CalendarRule]: return rules -def load_rule(rule_config: dict) -> CalendarRule: +def load_rule(rule_config: dict[str, Any]) -> CalendarRule: options = rule_config["options"] if "options" in rule_config else {} if rule_config["name"] == "exclude_all": diff --git a/notify/calendar/rules/rule.py b/notify/calendar/rules/rule.py index a27ff93b0dd38fce41dc4265340745338040f00d..dc27bd2757910c5b744f4d86d96f9d9bbf9f380c 100644 --- a/notify/calendar/rules/rule.py +++ b/notify/calendar/rules/rule.py @@ -9,7 +9,7 @@ class RuleResult(): class CalendarRule(): - def __init__(self): + def __init__(self) -> None: pass def apply(self, day: date) -> RuleResult: diff --git a/notify/channels/__init__.py b/notify/channels/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notify/channels/channel.py b/notify/channels/channel.py index 2ef65e42dd96ebd5fd3c3744e571d980ae96ebbc..8ac0c6a9cec5693aca7da57419f6efcd34437d36 100644 --- a/notify/channels/channel.py +++ b/notify/channels/channel.py @@ -2,8 +2,8 @@ from ..messages.notification import Notification class NotificationChannel: - def __init__(self): + def __init__(self) -> None: pass - def send_notification(message: Notification): + def send_notification(self, message: Notification) -> None: pass diff --git a/notify/channels/email.py b/notify/channels/email.py index 4ab6df13b1d7122fcb1b28dd50cf5fee8306b76f..e88d076d72897643264096c28dc03fea44d14b4d 100644 --- a/notify/channels/email.py +++ b/notify/channels/email.py @@ -2,7 +2,7 @@ from typing import Optional from .channel import NotificationChannel from ..messages.notification import Notification -from redmail import EmailSender +from redmail.email.sender import EmailSender class EmailChannel(NotificationChannel): @@ -10,7 +10,7 @@ class EmailChannel(NotificationChannel): self, smtp_username: str, smtp_password: str, smtp_server: str, smtp_port: int, from_email: str, to_emails: list[str], local_hostname: Optional[str] = None - ): + ) -> None: self._from_email = from_email self._to_emails = to_emails @@ -22,9 +22,9 @@ class EmailChannel(NotificationChannel): local_hostname=local_hostname ) - def send_notification(self, message: Notification): + def send_notification(self, message: Notification) -> None: try: - self._client.connect() + self._client.connect() # type: ignore for to_email in self._to_emails: print("Sending emails to", to_email) try: @@ -36,6 +36,6 @@ class EmailChannel(NotificationChannel): ) except Exception as e: print(f"Failed to send email: {e}") - self._client.close() + self._client.close() # type: ignore except Exception as e: print(f"Failed to send email: {e}") diff --git a/notify/channels/matrix.py b/notify/channels/matrix.py index 289f06080d84cbac7b2f5a5694c94159dcb6bb55..97ac7b6721665ea572f0e0407b9fb51b1e78c7e8 100644 --- a/notify/channels/matrix.py +++ b/notify/channels/matrix.py @@ -6,14 +6,14 @@ from ..messages.notification import Notification class MatrixMessageChannel(NotificationChannel): - def __init__(self, bot_server: str, instance_id: str, secret: str, room_ids: list[str]): + def __init__(self, bot_server: str, instance_id: str, secret: str, room_ids: list[str]) -> None: super().__init__() self._bot_server = bot_server self._instance_id = instance_id self._secret = secret self.room_ids = room_ids - def send_notification(self, message: Notification): + def send_notification(self, message: Notification) -> None: for room_id in self.room_ids: print("Sending message to room", room_id) diff --git a/notify/channels/vonage_sms.py b/notify/channels/vonage_sms.py index b970e3a3cb4bdbf0d3f64174d821a5f95ccf7736..80b8fd64a282f69c9ef62340781484a90f2e4914 100644 --- a/notify/channels/vonage_sms.py +++ b/notify/channels/vonage_sms.py @@ -5,13 +5,13 @@ from ..messages.notification import Notification class VonageSmsChannel(NotificationChannel): - def __init__(self, key: str, secret: str, sender: str, to_numbers: list[str]): + def __init__(self, key: str, secret: str, sender: str, to_numbers: list[str]) -> None: super().__init__() self._client = nexmo.Client(key=key, secret=secret) self._numbers = to_numbers self._sender = sender - def send_notification(self, message: Notification): + def send_notification(self, message: Notification) -> None: for number in self._numbers: print("Sending SMS to", number) try: diff --git a/notify/config.py b/notify/config.py index 2283ec9acf6012fcf89a4f770b3cb1abf318203e..3470f661dea77a7906b420eee4664eb96f3787aa 100644 --- a/notify/config.py +++ b/notify/config.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Any, Generator, Union import yaml from deepmerge import always_merger @@ -15,8 +15,8 @@ CHANNEL_TYPES = { } -def load_config(files: Union[str, list[str]]): - config = {} +def load_config(files: Union[str, list[str]]) -> Any: + config: Any = {} if isinstance(files, str): files = [files] @@ -29,7 +29,7 @@ def load_config(files: Union[str, list[str]]): return config -def build_channels(config) -> list[NotificationChannel]: +def build_channels(config: Any) -> Generator[NotificationChannel, None, None]: for channel in config["channels"]: type = channel["type"] if type not in CHANNEL_TYPES: diff --git a/notify/messages/__init__.py b/notify/messages/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notify/messages/__tests__/test_generator.py b/notify/messages/__tests__/test_generator.py index 5dc23dd10ec4f815e599834e067a8b774e35f423..3c578efea9ea9f1e340eeee3714e3282d1f2a0e3 100644 --- a/notify/messages/__tests__/test_generator.py +++ b/notify/messages/__tests__/test_generator.py @@ -1,3 +1,4 @@ +from typing import Any from pytest import fixture from datetime import date @@ -8,7 +9,7 @@ from notify.messages.generator import build_notification @fixture -def basic_timeline_iterator(): +def basic_timeline_iterator() -> BasicTimelineIterator: return BasicTimelineIterator( workday_calendar=Calendar([ "exclude_all", @@ -31,7 +32,7 @@ def basic_timeline_iterator(): @fixture -def template_config(): +def template_config() -> Any: return { "message": { "title": "Daily standup", @@ -41,13 +42,14 @@ def template_config(): } -def test_weekend(basic_timeline_iterator: TimelineIterator, template_config): +def test_weekend(basic_timeline_iterator: TimelineIterator, template_config: Any) -> None: 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): +def test_message_generation(basic_timeline_iterator: TimelineIterator, template_config: Any) -> None: msg = build_notification(basic_timeline_iterator, template_config, date(2022, 9, 21)) + assert msg is not None assert msg.title == "Daily standup" assert msg.message == """Dear Team Tirith! diff --git a/notify/messages/generator.py b/notify/messages/generator.py index eae6588c4cf809a2fab5b3eded4722bd1b3752d3..48100487ff3547ea9590117c065e3874101a5f5a 100644 --- a/notify/messages/generator.py +++ b/notify/messages/generator.py @@ -1,3 +1,4 @@ +from typing import Any, Optional import jinja2 from datetime import date @@ -5,21 +6,21 @@ from ..timeline.iterator import TimelineIterator, CalendarDay from .notification import Notification -def render(tempalte_file, data) -> str: +def render(tempalte_file: str, data: Any) -> 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: +def day_to_data(day: CalendarDay) -> dict[str, str]: return { **day.__dict__, "dow": day.day.strftime('%A') } -def build_notification(iter: TimelineIterator, config, starting_day: date) -> Notification: +def build_notification(iter: TimelineIterator, config: Any, starting_day: date) -> Optional[Notification]: iter.set_day(starting_day) days: list[CalendarDay] = [] diff --git a/notify/timeline/__init__.py b/notify/timeline/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notify/timeline/__tests__/test_basic.py b/notify/timeline/__tests__/test_basic.py index 6299dc6aafc54f11d87d348b254fdefbad6b79af..fabe132134ef17c1fb4095c0aa93ddc4e4d4ea87 100644 --- a/notify/timeline/__tests__/test_basic.py +++ b/notify/timeline/__tests__/test_basic.py @@ -1,12 +1,13 @@ from pytest import fixture from datetime import date -from notify.timeline.basic import BasicTimelineIterator, FixedDay, CalendarDay +from notify.timeline.basic import BasicTimelineIterator, FixedDay +from notify.timeline.iterator import CalendarDay from notify.calendar.calendar import Calendar @fixture -def basic_timeline(): +def basic_timeline() -> BasicTimelineIterator: return BasicTimelineIterator( workday_calendar=Calendar([ "exclude_all", @@ -28,12 +29,12 @@ def basic_timeline(): ) -def test_fixed_day(basic_timeline: BasicTimelineIterator): +def test_fixed_day(basic_timeline: BasicTimelineIterator) -> None: 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): +def test_iteration(basic_timeline: BasicTimelineIterator) -> None: expected_items = [ CalendarDay(day=date(2022, 9, 21), moderator="a"), CalendarDay(day=date(2022, 9, 22), moderator="a"), @@ -54,7 +55,7 @@ def test_iteration(basic_timeline: BasicTimelineIterator): break -def test_far_in_future(basic_timeline: BasicTimelineIterator): +def test_far_in_future(basic_timeline: BasicTimelineIterator) -> None: expected_items = [ CalendarDay(day=date(2046, 9, 21), moderator="c"), CalendarDay(day=date(2046, 9, 24), moderator="c"), diff --git a/notify/timeline/basic.py b/notify/timeline/basic.py index 164b5c650334aa59d3d1c908fc29e8539076186f..f1f4e71f0fc3f4c2c33da8b1604ea737264a2af9 100644 --- a/notify/timeline/basic.py +++ b/notify/timeline/basic.py @@ -23,7 +23,7 @@ class BasicTimelineIterator(TimelineIterator): self._offset_days = self._get_initial_offset() - def _get_initial_offset(self): + def _get_initial_offset(self) -> int: return \ self._moderators.index(self._fixed_day.moderator) * self._moderator_days + \ self._moderator_days - self._fixed_day.days_left - 1 diff --git a/requirements.txt b/requirements.txt index eeed271b15cddb5a6ea8a6f61e22f5cd3dddef7b..dbf7d62daf07714036efa31c3fd66c91ba54e649 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,13 @@ redmail holidays deepmerge jinja2 +markdown # test pytest flake8 +mypy + +# types +types-requests +types-PyYAML