Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
8 changes: 7 additions & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ the [icalendar](https://github.com/collective/icalendar) package.
For user convenience, the library includes `holidays-ics` tool for generating iCalendar (.ics)
files from holiday calendars provided by the library. It supports country, subdivision,
and financial market calendars, holiday category filtering, localized holiday names,
year selection, and custom output file names.
year selection, custom output file names, and suffixes for generated output file names.

The tool can be run either as an installed command:

Expand Down Expand Up @@ -661,6 +661,12 @@ Generate a holiday calendar for multiple years and save it to a custom file:
holidays-ics US --years 2025-2027 --output us_holidays.ics
```

Append a custom suffix to the generated file name:

```shell
holidays-ics CH --subdiv ZH --years 2025 --output-suffix -personal.ics
```

Generate a calendar containing only bank holidays:

```shell
Expand Down
31 changes: 28 additions & 3 deletions holidays/generate_ics.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ def __init__(self):
parser.add_argument(
"-l", "--language", help="Language code for holiday names (e.g., en_US, es)"
)
parser.add_argument("-o", "--output", help="Output file path (e.g., holidays.ics)")

output_group = parser.add_mutually_exclusive_group()
output_group.add_argument("-o", "--output", help="Output file path (e.g., holidays.ics)")
output_group.add_argument(
"--output-suffix",
help="Suffix to append to the generated output filename base (e.g., -MYNOTE.ics)",
)
Comment on lines +67 to +70

list_group = parser.add_mutually_exclusive_group()
list_group.add_argument(
Expand All @@ -73,7 +79,23 @@ def __init__(self):
list_group.add_argument(
"--list-languages", action="store_true", help="List supported languages"
)
self.args = parser.parse_args()
self.args = parser.parse_args(self.normalize_output_suffix_args(sys.argv[1:]))

@staticmethod
def normalize_output_suffix_args(args: list[str]) -> list[str]:
"""Allow suffix values that begin with '-' without requiring '=' syntax."""
normalized_args = list(args)
try:
option_index = normalized_args.index("--output-suffix")
except ValueError:
return normalized_args

value_index = option_index + 1
Comment thread
KJhellico marked this conversation as resolved.
if value_index < len(normalized_args):
suffix = normalized_args[value_index]
if suffix.startswith("-") and not suffix.startswith("--"):
normalized_args[option_index : value_index + 1] = [f"--output-suffix={suffix}"]
return normalized_args

@staticmethod
def parse_years(years: str) -> tuple[int, int]:
Expand Down Expand Up @@ -196,7 +218,10 @@ def run(self) -> None:
start_year, end_year = self.args.years
years_part = f"{start_year}_{end_year}" if start_year != end_year else f"{start_year}"
subdiv_part = f"_{self.args.subdiv.upper().replace(' ', '_')}" if self.args.subdiv else ""
output_path = self.args.output or f"{self.args.code}{subdiv_part}_{years_part}.ics"
output_suffix = self.args.output_suffix or ".ics"
Comment thread
KJhellico marked this conversation as resolved.
Outdated
output_path = (
self.args.output or f"{self.args.code}{subdiv_part}_{years_part}{output_suffix}"
)

try:
holiday_obj = self.entity_loader(
Expand Down
54 changes: 54 additions & 0 deletions tests/test_generate_ics.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,30 @@ def test_parse_categories(self):
IcsGenerator.parse_categories("BANK,Public,optional"), ["bank", "public", "optional"]
)

def test_normalize_output_suffix_args(self):
self.assertEqual(
IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix", "-personal.ics"]),
["US", "--output-suffix=-personal.ics"],
)
self.assertEqual(
IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix", "personal.ics"]),
["US", "--output-suffix", "personal.ics"],
)
self.assertEqual(
IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix=-personal.ics"]),
["US", "--output-suffix=-personal.ics"],
)
self.assertEqual(
IcsGenerator.normalize_output_suffix_args(
["US", "--output-suffix", "--list-categories"]
),
["US", "--output-suffix", "--list-categories"],
)
self.assertEqual(
IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix"]),
["US", "--output-suffix"],
)

def test_validate_country_code(self):
with self.argv("US"):
generator = IcsGenerator()
Expand Down Expand Up @@ -425,6 +449,36 @@ def test_filename_subdivision(self):

self.assertTrue((temp_dir / "US_CA_2025.ics").exists())

def test_filename_output_suffix(self):
with self.temp_cwd() as temp_dir:
with self.argv(
"CH", "--subdiv", "ZH", "--years", "2020", "--output-suffix", "-MYNOTE.ics"
):
IcsGenerator().run()

self.assertTrue((temp_dir / "CH_ZH_2020-MYNOTE.ics").exists())
self.assertFalse((temp_dir / "CH_ZH_2020.ics").exists())

def test_filename_output_suffix_does_not_append_extension(self):
with self.temp_cwd() as temp_dir:
with self.argv("US", "--years", "2025", "--output-suffix", "-personal"):
IcsGenerator().run()

self.assertTrue((temp_dir / "US_2025-personal").exists())
self.assertFalse((temp_dir / "US_2025-personal.ics").exists())

def test_filename_output_and_suffix_are_mutually_exclusive(self):
with (
self.argv("US", "--output", "calendar.ics", "--output-suffix", "-personal.ics"),
self.assertRaises(SystemExit),
):
IcsGenerator()

Comment thread
KJhellico marked this conversation as resolved.
Outdated
def test_output_suffix_equals_form_is_unchanged(self):
args = IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix=-personal.ics"])

self.assertEqual(args, ["US", "--output-suffix=-personal.ics"])

Comment thread
KJhellico marked this conversation as resolved.
Outdated
Comment thread
KJhellico marked this conversation as resolved.
Outdated
def test_generate_calendar_error(self):
with patch(
"holidays.ical.ICalExporter.save_ics",
Expand Down