-
-
Notifications
You must be signed in to change notification settings - Fork 705
Update iCalendar generation tool: add output filename template support #3679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 4 commits
797c7ef
3854ada
e939b4d
8b46fac
dd29d03
f181529
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |
| # Website: https://github.com/vacanza/holidays | ||
| # License: MIT (see LICENSE file) | ||
|
|
||
| import re | ||
| import sys | ||
| from argparse import ArgumentParser, ArgumentTypeError, Namespace | ||
| from collections.abc import Callable | ||
|
|
@@ -61,7 +62,17 @@ | |
| 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") | ||
| output_group.add_argument( | ||
| "--output-template", | ||
| help=( | ||
| "Output filename template. Available placeholders: {code}, {subdiv}, " | ||
| "{language}, {categories}, {start_year}, {end_year}, {today}. " | ||
| "Use '{{' and '}}' for literal '{' and '}'" | ||
| ), | ||
| ) | ||
|
|
||
| list_group = parser.add_mutually_exclusive_group() | ||
| list_group.add_argument( | ||
|
|
@@ -166,6 +177,27 @@ | |
| "Use --list-languages to see supported values" | ||
| ) | ||
|
|
||
| def validate_output_template(self, placeholders: set[str]) -> None: | ||
| if not self.args.output_template: | ||
| return None | ||
|
|
||
| template = self.args.output_template | ||
| tokens = re.findall(r"[^{}]+|\{\{|\}\}|\{[a-z_]+\}", template) | ||
Check failureCode scanning / SonarCloud Regular expressions should not cause catastrophic backtracking High
Make sure the regex used here, which is vulnerable to exponential runtime due to backtracking, cannot lead to denial of service. See more on SonarQube Cloud
|
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| if "".join(tokens) != template: | ||
| raise SystemExit("Invalid output template") | ||
|
|
||
| fields = re.findall(r"\{([a-z_]+)\}", template) | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| if not fields: | ||
| raise SystemExit("Output template must contain at least one placeholder") | ||
|
|
||
| for field_name in fields: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if field_name not in placeholders: | ||
| supported = ", ".join(f"{{{p}}}" for p in sorted(placeholders)) | ||
| raise SystemExit( | ||
| f"Unknown placeholder '{{{field_name}}}' in output template. " | ||
| f"Supported placeholders: {supported}" | ||
| ) | ||
|
|
||
| def handle_list_options(self) -> bool: | ||
| if self.args.list_subdivisions: | ||
| print(f"Supported subdivisions for {self.args.code}:") | ||
|
|
@@ -184,6 +216,26 @@ | |
|
|
||
| return False | ||
|
|
||
| def get_default_output_template(self) -> str: | ||
| start_year, end_year = self.args.years | ||
| parts = ["{code}"] | ||
|
|
||
| if self.args.subdiv: | ||
| parts.append("{subdiv}") | ||
|
|
||
| if self.args.language: | ||
| parts.append("{language}") | ||
|
|
||
| if self.args.categories: | ||
| parts.append("{categories}") | ||
|
|
||
| parts.append("{start_year}") | ||
|
|
||
| if start_year != end_year: | ||
| parts.append("{end_year}") | ||
|
|
||
| return "_".join(parts) + ".ics" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not good, str |
||
|
|
||
| def run(self) -> None: | ||
| self.validate_code() | ||
| if self.handle_list_options(): | ||
|
|
@@ -194,9 +246,24 @@ | |
| self.validate_categories() | ||
|
|
||
| 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" | ||
|
|
||
| if self.args.output: | ||
| output_path = self.args.output | ||
| else: | ||
| values = { | ||
| "code": self.args.code, | ||
| "subdiv": (self.args.subdiv or "ALL").upper().replace(" ", "_"), | ||
| "language": self.args.language.upper() if self.args.language else "DEFAULT", | ||
| "categories": ( | ||
| "_".join(self.args.categories).upper() if self.args.categories else "PUBLIC" | ||
| ), | ||
| "start_year": start_year, | ||
| "end_year": end_year, | ||
| "today": datetime.now(timezone.utc).strftime("%Y%m%d"), | ||
| } | ||
| self.validate_output_template(set(values)) | ||
| template = self.args.output_template or self.get_default_output_template() | ||
| output_path = template.format(**values) | ||
|
|
||
| try: | ||
| holiday_obj = self.entity_loader( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.