Skip to content

Add generic holiday calendar export script (examples/vacanza.py)#3449

Open
YashThosar wants to merge 3 commits intovacanza:devfrom
YashThosar:dev
Open

Add generic holiday calendar export script (examples/vacanza.py)#3449
YashThosar wants to merge 3 commits intovacanza:devfrom
YashThosar:dev

Conversation

@YashThosar
Copy link
Copy Markdown

@YashThosar YashThosar commented Mar 29, 2026

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:

  • Dynamic category detection using supported_categories
  • Optional year argument (single year or range e.g. 2020-2025)
  • Optional language argument for localized .ics output
  • Optional public-holidays filter to exclude de-facto holidays
  • Error message with usage examples when no arguments provided

Copilot AI review requested due to automatic review settings March 29, 2026 06:52
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

Summary by CodeRabbit

  • New Features
    • New command-line tool to generate iCalendar holiday files (.ics) for any supported country. Supports specifying year ranges, language selection, and filtering to public holidays only. Files can be imported directly into calendar applications.

Walkthrough

Adds a new executable example script examples/vacanza.py that parses year (single or range), validates country codes, iterates supported holiday categories (optionally public-only), and exports per-category iCalendar (.ics) files via ICalExporter, with a CLI entrypoint.

Changes

Cohort / File(s) Summary
New Example Script
examples/vacanza.py
New CLI script: parse_years(year_arg) for YYYY or YYYY-YYYY; generate(country_code, years=None, language=None, public_only=False) validates country, selects categories (optional PUBLIC-only), reloads holiday data, builds filenames with country/category/year_tag/language, writes .ics with ICalExporter, prints generated paths; adds __main__ usage handling.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested labels

script

Suggested reviewers

  • arkid15r
  • KJhellico
  • PPsyrius
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: introducing a generic holiday calendar export script in examples/vacanza.py.
Description check ✅ Passed The description is directly related to the changeset, detailing the script's purpose, usage examples, and key features that align with the code changes.
Linked Issues check ✅ Passed The implementation meets all primary coding requirements from #3189: accepts country code, dynamically queries supported_categories, generates .ics files, supports year/year-range arguments, accepts optional language argument, filters public holidays, and prints usage messages.
Out of Scope Changes check ✅ Passed All changes are in-scope: the new script examples/vacanza.py with its functions (parse_years, generate) directly address the linked issue requirements without introducing unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py script that accepts a country code and optional year.
  • Iterates over an entity’s supported_categories to export one .ics per category.
  • Adds a public-holidays flag intended to restrict exports to public holidays only.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

import holidays
from holidays.ical import ICalExporter

def generate(country_code, year=2025, public_only=False):
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +31
year = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else 2025
public_only = "public-holidays" in sys.argv

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +7
country_class = holidays.country_holidays(country_code)
categories = country_class.supported_categories
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +7
country_class = holidays.country_holidays(country_code)
categories = country_class.supported_categories
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
categories = country_class.supported_categories

if public_only:
categories = [c for c in categories if c == "PUBLIC"]
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
categories = [c for c in categories if c == "PUBLIC"]
categories = [c for c in categories if c == holidays.constants.PUBLIC]

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 18b9f84 and 78cb4e4.

📒 Files selected for processing (1)
  • examples/vacanza.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
examples/vacanza.py (2)

22-23: ⚠️ Potential issue | 🔴 Critical

public-holidays filter is broken and skips all output.

At Line 23, filtering with "PUBLIC" won’t match supported_categories values ("public"), so categories becomes 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 | 🟠 Major

Year parsing is too permissive, and the default year is stale.

parse_years() accepts malformed/reversed ranges without clear CLI errors, and defaults are hardcoded to 2025 (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

📥 Commits

Reviewing files that changed from the base of the PR and between 78cb4e4 and 02bbe1a.

📒 Files selected for processing (1)
  • examples/vacanza.py

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
examples/vacanza.py (1)

11-22: ⚠️ Potential issue | 🟠 Major

Harden 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 as language instead 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 = arg

Also 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

📥 Commits

Reviewing files that changed from the base of the PR and between 02bbe1a and 4f19b0f.

📒 Files selected for processing (1)
  • examples/vacanza.py

@KJhellico
Copy link
Copy Markdown
Collaborator

For practical use, the following features are missing, at least:

  • financial markets support
  • subdivisions support
  • detailed validation of user-entered values

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature request] vacanza-holidays.py to generate calendars for any country, year, etc

3 participants