From db27e9affaf81671c127cc381c62365c52480b8f Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Sun, 31 May 2026 05:10:44 -0700 Subject: [PATCH 1/2] treat gapped events as invalid input, raise value error --- LICENSE | 2 +- bin/drawcal | 2 +- lib/drawcal/__init__.py | 2 +- lib/drawcal/cli.py | 2 +- lib/drawcal/config.py | 2 +- lib/drawcal/drawlib.py | 5 ++++- lib/drawcal/events.py | 45 ++++++++++++++++++++++++++++------------- tests/test_events.py | 18 ++++++++++++++++- tests/test_render.py | 8 ++++++++ 9 files changed, 65 insertions(+), 21 deletions(-) diff --git a/LICENSE b/LICENSE index 4f39123..b654b89 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ drawcal -Copyright (c) 2022-2025, Bnbnotify +Copyright (c) 2022-2026, Bnbnotify All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/bin/drawcal b/bin/drawcal index d65c0d0..35b94af 100755 --- a/bin/drawcal +++ b/bin/drawcal @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2022-2025, Bnbnotify +# Copyright (c) 2022-2026, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/lib/drawcal/__init__.py b/lib/drawcal/__init__.py index cae2a5c..046aaef 100644 --- a/lib/drawcal/__init__.py +++ b/lib/drawcal/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Bnbnotify +# Copyright (c) 2022-2026, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/lib/drawcal/cli.py b/lib/drawcal/cli.py index 381a5dd..7f3d4b0 100755 --- a/lib/drawcal/cli.py +++ b/lib/drawcal/cli.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Bnbnotify +# Copyright (c) 2022-2026, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/lib/drawcal/config.py b/lib/drawcal/config.py index 68f3661..ec15b57 100644 --- a/lib/drawcal/config.py +++ b/lib/drawcal/config.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Bnbnotify +# Copyright (c) 2022-2026, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/lib/drawcal/drawlib.py b/lib/drawcal/drawlib.py index 67c7427..da35ef7 100644 --- a/lib/drawcal/drawlib.py +++ b/lib/drawcal/drawlib.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Bnbnotify +# Copyright (c) 2022-2026, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -38,6 +38,7 @@ from PIL import Image, ImageFont, ImageDraw from drawcal import config +from drawcal.events import validate_events # set some global date values _d = datetime.today() @@ -115,6 +116,8 @@ def draw_calendar( # make sure events is a list if events is None: events = [] + else: + validate_events(events) # categorize and track dates conflict_dates = set() diff --git a/lib/drawcal/events.py b/lib/drawcal/events.py index 27cec54..4ae8a89 100644 --- a/lib/drawcal/events.py +++ b/lib/drawcal/events.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Bnbnotify +# Copyright (c) 2022-2026, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -43,6 +43,32 @@ delta = timedelta(days=1) +def validate_events(events): + """Validate drawcal event payloads.""" + + if not isinstance(events, list): + raise ValueError("events must be a list of event lists") + + for event in events: + if not isinstance(event, list): + raise ValueError("each event must be a list of date strings") + + parsed_days = [] + for day in event: + if not isinstance(day, str): + raise ValueError("each event date must be a string in M/D/YYYY format") + try: + parsed_days.append(datetime.strptime(day, "%m/%d/%Y")) + except (TypeError, ValueError) as exc: + raise ValueError(f"invalid event date: {day}") from exc + + for previous, current in zip(parsed_days, parsed_days[1:]): + if current <= previous: + raise ValueError("event dates must be in strictly increasing order") + if current - previous != delta: + raise ValueError("event dates must be consecutive with no gaps") + + def get_events(month=today.month, year=today.year): """Returns a list of randomly generated events.""" from random import randint @@ -86,18 +112,9 @@ def read_events(filepath): except json.JSONDecodeError as exc: raise ValueError(f"invalid JSON in events file: {filepath}") from exc - if not isinstance(events, list): - raise ValueError("events file must contain a list of event lists") - - for event in events: - if not isinstance(event, list): - raise ValueError("each event must be a list of date strings") - for day in event: - if not isinstance(day, str): - raise ValueError("each event date must be a string in M/D/YYYY format") - try: - datetime.strptime(day, "%m/%d/%Y") - except (TypeError, ValueError) as exc: - raise ValueError(f"invalid event date: {day}") from exc + try: + validate_events(events) + except ValueError as exc: + raise ValueError(f"invalid events file: {exc}") from exc return events diff --git a/tests/test_events.py b/tests/test_events.py index a67b7dc..558c4dc 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -3,7 +3,7 @@ import unittest from datetime import datetime -from drawcal.events import get_events, read_events +from drawcal.events import get_events, read_events, validate_events class ReadEventsTests(unittest.TestCase): @@ -27,6 +27,16 @@ def test_read_events_rejects_invalid_dates(self): with self.assertRaises(ValueError): read_events(path) + def test_read_events_rejects_gapped_events(self): + with tempfile.TemporaryDirectory() as tmpdir: + path = f"{tmpdir}/events.json" + + with open(path, "w", encoding="utf-8") as fp: + json.dump([["6/3/2022", "6/6/2022", "6/7/2022"]], fp) + + with self.assertRaisesRegex(ValueError, "consecutive with no gaps"): + read_events(path) + class GetEventsTests(unittest.TestCase): def test_get_events_stays_within_month(self): @@ -38,3 +48,9 @@ def test_get_events_stays_within_month(self): self.assertEqual(parsed.month, 2) self.assertEqual(parsed.year, 2025) self.assertLessEqual(parsed.day, 28) + + +class ValidateEventsTests(unittest.TestCase): + def test_validate_events_rejects_gapped_direct_input(self): + with self.assertRaisesRegex(ValueError, "consecutive with no gaps"): + validate_events([["6/3/2022", "6/6/2022", "6/7/2022"]]) diff --git a/tests/test_render.py b/tests/test_render.py index c5747d2..d39bf92 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -34,3 +34,11 @@ def test_draw_calendar_matches_expected_image(self): self.assertIsNone( diff.getbbox(), "rendered image does not match fixture" ) + + def test_draw_calendar_rejects_gapped_events(self): + with self.assertRaisesRegex(ValueError, "consecutive with no gaps"): + draw_calendar( + month=6, + year=2022, + events=[["6/3/2022", "6/6/2022", "6/7/2022"]], + ) From c6e70b89f9d638641767a9bade52fc2726f3ea62 Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Sun, 31 May 2026 05:11:42 -0700 Subject: [PATCH 2/2] Bump version to 0.5.9 --- lib/drawcal/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/drawcal/__init__.py b/lib/drawcal/__init__.py index 046aaef..066a180 100644 --- a/lib/drawcal/__init__.py +++ b/lib/drawcal/__init__.py @@ -34,7 +34,7 @@ """ __prog__ = "drawcal" -__version__ = "0.5.8" +__version__ = "0.5.9" __author__ = "ryan@rsgalloway.com" diff --git a/pyproject.toml b/pyproject.toml index e0ae66a..b89865c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "drawcal" -version = "0.5.8" +version = "0.5.9" description = "Python library for drawing simple monthly calendar images with events." readme = "README.md" requires-python = ">=3.8"