Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion bin/drawcal
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
4 changes: 2 additions & 2 deletions lib/drawcal/__init__.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -34,7 +34,7 @@
"""

__prog__ = "drawcal"
__version__ = "0.5.8"
__version__ = "0.5.9"
__author__ = "ryan@rsgalloway.com"


Expand Down
2 changes: 1 addition & 1 deletion lib/drawcal/cli.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion lib/drawcal/config.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
5 changes: 4 additions & 1 deletion lib/drawcal/drawlib.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
45 changes: 31 additions & 14 deletions lib/drawcal/events.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 17 additions & 1 deletion tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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"]])
8 changes: 8 additions & 0 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]],
)
Loading