Add generic holiday calendar export script (examples/vacanza.py)#3449
Add generic holiday calendar export script (examples/vacanza.py)#3449YashThosar wants to merge 3 commits intovacanza:devfrom
Conversation
Summary by CodeRabbit
WalkthroughAdds a new executable example script Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a generic, country-parameterized example script to export holiday calendars as .ics files using the project’s holidays entities and ICalExporter, aligning with the feature request in #3189 for a reusable generator.
Changes:
- Adds
examples/vacanza.pyscript that accepts a country code and optional year. - Iterates over an entity’s
supported_categoriesto export one.icsper category. - Adds a
public-holidaysflag intended to restrict exports to public holidays only.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
examples/vacanza.py
Outdated
| import holidays | ||
| from holidays.ical import ICalExporter | ||
|
|
||
| def generate(country_code, year=2025, public_only=False): |
There was a problem hiding this comment.
The script hard-codes 2025 as the default year (both in generate() and the CLI parsing). This will become stale and surprising over time; consider defaulting to the current year (e.g., date.today().year) so running the script without a year stays useful.
examples/vacanza.py
Outdated
| year = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else 2025 | ||
| public_only = "public-holidays" in sys.argv | ||
|
|
There was a problem hiding this comment.
The CLI parsing silently falls back to the default year whenever the second argument is present but not purely digits (e.g., a typo like 20O5), which can hide user errors. It would be more robust to validate the arguments (or use argparse) and emit a clear error/usage message when an unrecognized year/category is provided.
| year = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else 2025 | |
| public_only = "public-holidays" in sys.argv | |
| year = 2025 | |
| public_only = False | |
| # Parse remaining CLI arguments: optional YEAR and "public-holidays". | |
| for arg in sys.argv[2:]: | |
| if arg.isdigit(): | |
| if year != 2025: | |
| print("Error: multiple year arguments provided.") | |
| print("Usage: python examples/vacanza.py <COUNTRY_CODE> [YEAR] [public-holidays]") | |
| sys.exit(1) | |
| year = int(arg) | |
| elif arg == "public-holidays": | |
| public_only = True | |
| else: | |
| print(f"Error: unrecognized argument: {arg}") | |
| print("Usage: python examples/vacanza.py <COUNTRY_CODE> [YEAR] [public-holidays]") | |
| sys.exit(1) |
examples/vacanza.py
Outdated
| country_class = holidays.country_holidays(country_code) | ||
| categories = country_class.supported_categories |
There was a problem hiding this comment.
country_class is an instance returned by holidays.country_holidays(...), not a class. Renaming it (e.g., country_holidays_obj / instance) would avoid confusion when reading/maintaining the script.
| country_class = holidays.country_holidays(country_code) | |
| categories = country_class.supported_categories | |
| country_holidays_instance = holidays.country_holidays(country_code) | |
| categories = country_holidays_instance.supported_categories |
examples/vacanza.py
Outdated
| country_class = holidays.country_holidays(country_code) | ||
| categories = country_class.supported_categories |
There was a problem hiding this comment.
If an unsupported/invalid country_code is provided, holidays.country_holidays(country_code) raises NotImplementedError and the script will terminate with a traceback. Consider catching this and printing a user-friendly message (optionally including holidays.list_supported_countries(...)) with a non-zero exit code.
examples/vacanza.py
Outdated
| categories = country_class.supported_categories | ||
|
|
||
| if public_only: | ||
| categories = [c for c in categories if c == "PUBLIC"] |
There was a problem hiding this comment.
The public-holidays option will never work because supported category values are lowercase (e.g., "public" from holidays.constants.PUBLIC), but the filter compares against the uppercase string "PUBLIC", resulting in an empty category list and no files generated.
| categories = [c for c in categories if c == "PUBLIC"] | |
| categories = [c for c in categories if c == holidays.constants.PUBLIC] |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/vacanza.py`:
- Around line 24-25: Update the script's usage text printed by the two print
statements so it documents the additional invocation forms: include a year-range
form (e.g., "IN 2020-2025") and the public-only flag form (e.g., "IN 2025
public-holidays"); modify the strings that currently print "Usage: python
examples/vacanza.py <COUNTRY_CODE> [YEAR] [public-holidays]" and the example
line "Example: python examples/vacanza.py IN 2025" to show both "IN 2020-2025"
and "IN 2025 public-holidays" usages so users can discover year-range and
public-only CLI behavior.
- Line 5: The generate function currently defaults year to 2025 and treats
non-digit input silently; change the default to 2026 and implement strict year
parsing in generate: accept either a single year (int or "YYYY") or a range
"YYYY-YYYY", validate both years are four-digit integers and that start <= end,
convert to ints (for a range return or iterate as needed by the rest of the
function), and raise a ValueError (or a usage error) immediately for any invalid
format (e.g., non-digit parts, wrong length, start > end) instead of falling
back to a stale default; reference the generate(...) signature and the current
parsing code path so you update the parser in the same function.
- Around line 9-10: The public_only branch incorrectly compares categories to
the literal "PUBLIC" while supported_categories use lowercase; import the
lowercase category constant from holidays.constants (the constant used for the
"public" category) and replace the string comparison in the public_only filter
(the categories list comprehension) to compare against that imported constant
instead of the hardcoded "PUBLIC".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: efc487aa-ff3e-425b-b452-e0309fe8ecbb
📒 Files selected for processing (1)
examples/vacanza.py
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
examples/vacanza.py (2)
22-23:⚠️ Potential issue | 🔴 Critical
public-holidaysfilter is broken and skips all output.At Line 23, filtering with
"PUBLIC"won’t matchsupported_categoriesvalues ("public"), socategoriesbecomes empty and the loop at Line 25 generates nothing.Proposed fix
import holidays +from holidays.constants import PUBLIC from holidays.ical import ICalExporter @@ if public_only: - categories = [c for c in categories if c == "PUBLIC"] + categories = [c for c in categories if c == PUBLIC]#!/bin/bash # Verify category constant value and current filter expression. rg -n '^PUBLIC\s*=' holidays/constants.py rg -n 'supported_categories|c == "PUBLIC"|c == PUBLIC' examples/vacanza.py holidays/holiday_base.py🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/vacanza.py` around lines 22 - 23, The filter for public_only is comparing to the wrong constant string so it empties categories; update the condition in the examples/vacanza.py block that uses public_only to compare against the actual category value (use the defined PUBLIC constant or compare case-insensitively) — locate the public_only check and the categories comprehension and replace c == "PUBLIC" with either c == PUBLIC (importing PUBLIC from holidays.constants) or c.lower() == "public" so it matches supported_categories correctly.
6-10:⚠️ Potential issue | 🟠 MajorYear parsing is too permissive, and the default year is stale.
parse_years()accepts malformed/reversed ranges without clear CLI errors, and defaults are hardcoded to2025(Lines 13 and 54). As of March 29, 2026, that default is outdated.Proposed fix
+from datetime import date @@ -def parse_years(year_arg): - if "-" in year_arg: - start, end = year_arg.split("-") - return range(int(start), int(end) + 1) - return int(year_arg) +def parse_years(year_arg: str) -> int | range: + if year_arg.isdigit() and len(year_arg) == 4: + return int(year_arg) + + parts = year_arg.split("-", 1) + if len(parts) == 2 and all(p.isdigit() and len(p) == 4 for p in parts): + start, end = map(int, parts) + if start > end: + raise ValueError("YEAR_RANGE start must be <= end") + return range(start, end + 1) + + raise ValueError("YEAR must be YYYY or YYYY-YYYY") @@ -def generate(country_code, years=2025, language=None, public_only=False): +def generate(country_code, years=None, language=None, public_only=False): + years = date.today().year if years is None else years @@ - years = 2025 + years = date.today().year @@ - elif arg.replace("-", "").isdigit() and len(arg) > 2: - years = parse_years(arg) + elif arg[:1].isdigit(): + years = parse_years(arg)Also applies to: 13-13, 54-54, 61-62
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/vacanza.py` around lines 6 - 10, parse_years() is too permissive and some defaults are stale: validate the input format and range and update hardcoded defaults to 2026. In parse_years, ensure the argument is either a single 4‑digit year or a properly formatted "YYYY-YYYY" range, parse ints, raise a ValueError with a clear message for malformed input (non‑numeric, wrong length, missing parts) and for reversed ranges (start > end), and return a consistent sequence (e.g., list of ints) rather than sometimes an int; update any callers expecting the old return type. Also replace hardcoded default year values set to 2025 with 2026 in the same module (update the default constants/usages mentioned around the earlier lines).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/vacanza.py`:
- Around line 1-3: The script imports the package before making the repo root
importable, which breaks when running python examples/vacanza.py; before
importing holidays or ICalExporter, add a sys.path.insert(0, str(Path.cwd()))
(with comment "# Make holidays visible.") so the project package is found when
examples/ is the working module; ensure you import Path from pathlib if not
present and place the sys.path adjustment immediately after the existing import
sys line and prior to "import holidays" and "from holidays.ical import
ICalExporter".
---
Duplicate comments:
In `@examples/vacanza.py`:
- Around line 22-23: The filter for public_only is comparing to the wrong
constant string so it empties categories; update the condition in the
examples/vacanza.py block that uses public_only to compare against the actual
category value (use the defined PUBLIC constant or compare case-insensitively) —
locate the public_only check and the categories comprehension and replace c ==
"PUBLIC" with either c == PUBLIC (importing PUBLIC from holidays.constants) or
c.lower() == "public" so it matches supported_categories correctly.
- Around line 6-10: parse_years() is too permissive and some defaults are stale:
validate the input format and range and update hardcoded defaults to 2026. In
parse_years, ensure the argument is either a single 4‑digit year or a properly
formatted "YYYY-YYYY" range, parse ints, raise a ValueError with a clear message
for malformed input (non‑numeric, wrong length, missing parts) and for reversed
ranges (start > end), and return a consistent sequence (e.g., list of ints)
rather than sometimes an int; update any callers expecting the old return type.
Also replace hardcoded default year values set to 2025 with 2026 in the same
module (update the default constants/usages mentioned around the earlier lines).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f74ac72d-28e9-47ae-b9c0-58b1e2c12853
📒 Files selected for processing (1)
examples/vacanza.py
|
There was a problem hiding this comment.
♻️ Duplicate comments (1)
examples/vacanza.py (1)
11-22:⚠️ Potential issue | 🟠 MajorHarden year parsing and fail fast on malformed year-like args.
parse_years()still accepts invalid shapes (non-4-digit years, reversed ranges), and the CLI router can treat malformed year-like inputs aslanguageinstead of erroring (Line 77). This can produce silent wrong output instead of a usage error.Proposed fix
def parse_years(year_arg): - if "-" in year_arg: - parts = year_arg.split("-") - if len(parts) == 2 and all(p.isdigit() for p in parts): - return range(int(parts[0]), int(parts[1]) + 1) - else: - print(f"Error: invalid year range '{year_arg}'. Use YYYY or YYYY-YYYY.") - sys.exit(1) - if year_arg.isdigit(): - return int(year_arg) - print(f"Error: invalid year '{year_arg}'. Use YYYY or YYYY-YYYY.") - sys.exit(1) + if year_arg.isdigit() and len(year_arg) == 4: + return int(year_arg) + + parts = year_arg.split("-") + if ( + len(parts) == 2 + and all(p.isdigit() and len(p) == 4 for p in parts) + ): + start, end = map(int, parts) + if start <= end: + return range(start, end + 1) + + print(f"Error: invalid year '{year_arg}'. Use YYYY or YYYY-YYYY.") + sys.exit(1) @@ for arg in sys.argv[2:]: if arg == "public-holidays": public_only = True - elif arg.replace("-", "").isdigit() and len(arg) >= 4: + elif arg[:1].isdigit(): years = parse_years(arg) else: language = argAlso applies to: 74-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/vacanza.py` around lines 11 - 22, parse_years currently accepts malformed inputs and returns inconsistent types causing the CLI router to misclassify bad year-like args; update parse_years to validate that each year is exactly four digits, that range bounds are numeric and start <= end, and fail fast with a clear process exit and error message on any invalid input; also normalize the return type (always return a range or list, e.g., range(start, end+1), even for a single year) so callers and the CLI router (the language-routing logic) cannot treat malformed year-like strings as language names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@examples/vacanza.py`:
- Around line 11-22: parse_years currently accepts malformed inputs and returns
inconsistent types causing the CLI router to misclassify bad year-like args;
update parse_years to validate that each year is exactly four digits, that range
bounds are numeric and start <= end, and fail fast with a clear process exit and
error message on any invalid input; also normalize the return type (always
return a range or list, e.g., range(start, end+1), even for a single year) so
callers and the CLI router (the language-routing logic) cannot treat malformed
year-like strings as language names.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6ac70789-8d2c-4bf8-9a3c-3f900ea9a935
📒 Files selected for processing (1)
examples/vacanza.py
|
For practical use, the following features are missing, at least:
|



Closes #3189
Implements a generic script that accepts a country code as a command-line
argument and generates .ics calendar files for all supported categories.
Usage:
python examples/vacanza.py IN
python examples/vacanza.py IN 2025
python examples/vacanza.py IN 2020-2025
python examples/vacanza.py IN 2025 mr
python examples/vacanza.py IN 2025 public-holidays
Features: