icalendar-events-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- icalendar_events_cli/__init__.py +1 -0
- icalendar_events_cli/__main__.py +59 -0
- icalendar_events_cli/argparse.py +207 -0
- icalendar_events_cli/argparse.py.orig +131 -0
- icalendar_events_cli/downloader.py +33 -0
- icalendar_events_cli/downloader.py.orig +36 -0
- icalendar_events_cli/icalendar.py +164 -0
- icalendar_events_cli/output.py +133 -0
- icalendar_events_cli-1.0.0.dist-info/METADATA +249 -0
- icalendar_events_cli-1.0.0.dist-info/RECORD +13 -0
- icalendar_events_cli-1.0.0.dist-info/WHEEL +4 -0
- icalendar_events_cli-1.0.0.dist-info/entry_points.txt +5 -0
- icalendar_events_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Module init."""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Commandline interface entry point."""
|
|
2
|
+
|
|
3
|
+
# ---- Imports --------------------------------------------------------------------------------------------------------
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .argparse import parse_config
|
|
9
|
+
from .downloader import download_ics
|
|
10
|
+
from .icalendar import filter_events, parse_calendar
|
|
11
|
+
from .output import output_events
|
|
12
|
+
|
|
13
|
+
# ---- Module Meta-Data ------------------------------------------------------------------------------------------------
|
|
14
|
+
__prog__ = "icalendar-events-cli"
|
|
15
|
+
__dist_name__ = "icalendar_events_cli"
|
|
16
|
+
__copyright__ = "Copyright 2023-2025"
|
|
17
|
+
__author__ = "Sebastian Waldvogel"
|
|
18
|
+
|
|
19
|
+
# ---- Main -----------------------------------------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cli(arg_list: Optional[list[str]] = None) -> int:
|
|
23
|
+
"""Main command line handling entry point.
|
|
24
|
+
|
|
25
|
+
Arguments:
|
|
26
|
+
arg_list: Optional list of command line arguments. Only needed for testing.
|
|
27
|
+
Productive __main__ will call the API without any argument.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Numeric exit code
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
config = parse_config(
|
|
34
|
+
prog=__prog__,
|
|
35
|
+
version=importlib.metadata.version(__dist_name__),
|
|
36
|
+
copy_right=__copyright__,
|
|
37
|
+
author=__author__,
|
|
38
|
+
arg_list=arg_list,
|
|
39
|
+
)
|
|
40
|
+
return _main_logic(config)
|
|
41
|
+
|
|
42
|
+
except SystemExit as e:
|
|
43
|
+
return e.code
|
|
44
|
+
|
|
45
|
+
except BaseException as e: # pylint: disable=broad-exception-caught;reason=Explicitely capture all exceptions thrown during execution.
|
|
46
|
+
print(
|
|
47
|
+
f"ERROR: Any error has occured!{os.linesep}{os.linesep}Exception: {str(e)}"
|
|
48
|
+
# f"Detailed Traceback: {traceback.format_exc()}"
|
|
49
|
+
)
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _main_logic(config: dict) -> int:
|
|
54
|
+
calendar_ics = download_ics(config.calendar)
|
|
55
|
+
events = parse_calendar(calendar_ics, config.filter)
|
|
56
|
+
events = filter_events(events, config.filter, config.calendar.encoding)
|
|
57
|
+
output_events(events, config)
|
|
58
|
+
|
|
59
|
+
return os.EX_OK
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Argument parsing."""
|
|
2
|
+
|
|
3
|
+
# ---- Imports ----
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from argparse import ArgumentTypeError
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import pytz
|
|
11
|
+
from jsonargparse import ArgumentParser, DefaultHelpFormatter
|
|
12
|
+
from pydantic import SecretStr
|
|
13
|
+
from rich_argparse import RawTextRichHelpFormatter
|
|
14
|
+
from tzlocal import get_localzone
|
|
15
|
+
|
|
16
|
+
from .output import OutputFormat
|
|
17
|
+
|
|
18
|
+
# ---- Globals ---------------------------------------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
_local_timezone = pytz.timezone(get_localzone().key)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---- CommandLine parser ----------------------------------------------------------------------------------------------
|
|
24
|
+
class HelpFormatter(DefaultHelpFormatter, RawTextRichHelpFormatter):
|
|
25
|
+
"""Custom CLI help formatter: Combined DefaultHelpFormatter and RichHelpFormatter."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_config(prog: str, version: str, copy_right: str, author: str, arg_list: Optional[list[str]] = None) -> dict:
|
|
29
|
+
"""Parse the configuration from CLI and/or configuration JSON file.
|
|
30
|
+
|
|
31
|
+
Arguments:
|
|
32
|
+
prog: Program name.
|
|
33
|
+
version: Program version.
|
|
34
|
+
copy_right: Copyright info.
|
|
35
|
+
author: Author info.
|
|
36
|
+
arg_list: Optional command line arguments list.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dict: Parsed configuration options.
|
|
40
|
+
"""
|
|
41
|
+
arg_parser = ArgumentParser(
|
|
42
|
+
prog=prog,
|
|
43
|
+
description="Command-line tool to read events from a iCalendar (ICS) files."
|
|
44
|
+
+ f" | Version {version} | {copy_right}",
|
|
45
|
+
version=f"| Version {version}\n{copy_right} {author}",
|
|
46
|
+
default_config_files=["./config.json"],
|
|
47
|
+
print_config=None,
|
|
48
|
+
env_prefix="ICALENDAR_EVENTS_CLI",
|
|
49
|
+
default_env=False,
|
|
50
|
+
formatter_class=HelpFormatter,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
arg_parser.add_argument("-c", "--config", action="config", help="""Path to JSON configuration file.""")
|
|
54
|
+
|
|
55
|
+
# ---- Calendar URL / access ----
|
|
56
|
+
arg_parser.add_argument(
|
|
57
|
+
"--calendar.url",
|
|
58
|
+
type=str,
|
|
59
|
+
required=True,
|
|
60
|
+
help="""URL of the iCalendar (ICS).
|
|
61
|
+
Also URLs to local files with schema file://<absolute path to local file> are supported.""",
|
|
62
|
+
)
|
|
63
|
+
arg_parser.add_argument(
|
|
64
|
+
"--calendar.verify-url",
|
|
65
|
+
type=bool,
|
|
66
|
+
default=True,
|
|
67
|
+
help="Configure SSL verification of the URL",
|
|
68
|
+
)
|
|
69
|
+
arg_parser.add_argument(
|
|
70
|
+
"--calendar.user",
|
|
71
|
+
type=SecretStr,
|
|
72
|
+
help="Username for calendar URL HTTP authentication (basic authentication)",
|
|
73
|
+
)
|
|
74
|
+
arg_parser.add_argument(
|
|
75
|
+
"--calendar.password",
|
|
76
|
+
type=SecretStr,
|
|
77
|
+
help="Password for calendar URL HTTP authentication (basic authentication)",
|
|
78
|
+
)
|
|
79
|
+
arg_parser.add_argument("--calendar.encoding", default="UTF-8", help="Encoding of the calendar")
|
|
80
|
+
|
|
81
|
+
# ---- Filtering ----
|
|
82
|
+
|
|
83
|
+
arg_parser.add_argument(
|
|
84
|
+
"-s",
|
|
85
|
+
"--filter.start-date",
|
|
86
|
+
type=datetime_isoformat,
|
|
87
|
+
default=_local_timezone.localize(datetime.now().replace(microsecond=0)).replace(microsecond=0),
|
|
88
|
+
help="Start date/time of event filter by time (ISO format). Default: now",
|
|
89
|
+
)
|
|
90
|
+
arg_parser.add_argument(
|
|
91
|
+
"-e",
|
|
92
|
+
"--filter.end-date",
|
|
93
|
+
type=datetime_isoformat,
|
|
94
|
+
default=_local_timezone.localize(datetime.combine(datetime.now(), datetime.max.time())).replace(microsecond=0),
|
|
95
|
+
help="End date/time of event filter by time (ISO format). Default: end of today",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
arg_parser.add_argument(
|
|
99
|
+
"-f",
|
|
100
|
+
"--filter.summary",
|
|
101
|
+
type=regex_type,
|
|
102
|
+
required=False,
|
|
103
|
+
default=None,
|
|
104
|
+
help="RegEx to filter calendar events based on the summary attribute.",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
arg_parser.add_argument(
|
|
108
|
+
"--filter.description",
|
|
109
|
+
type=regex_type,
|
|
110
|
+
required=False,
|
|
111
|
+
default=None,
|
|
112
|
+
help="RegEx to filter calendar events based on the description attribute.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
arg_parser.add_argument(
|
|
116
|
+
"--filter.location",
|
|
117
|
+
type=regex_type,
|
|
118
|
+
required=False,
|
|
119
|
+
default=None,
|
|
120
|
+
help="RegEx to filter calendar events based on the location attribute.",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# ---- Output ----
|
|
124
|
+
arg_parser.add_argument(
|
|
125
|
+
"--output.format",
|
|
126
|
+
default=OutputFormat.human_readable,
|
|
127
|
+
type=OutputFormat,
|
|
128
|
+
help="Output format.",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
arg_parser.add_argument(
|
|
132
|
+
"-o",
|
|
133
|
+
"--output.file",
|
|
134
|
+
type=Optional[str],
|
|
135
|
+
help="Path of JSON output file. If not set the output is written to console / stdout",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# ---- Finally parse the inputs ----
|
|
139
|
+
config = arg_parser.parse_args(args=arg_list)
|
|
140
|
+
|
|
141
|
+
# ---- Post-parse validation ----
|
|
142
|
+
_validate_config(config)
|
|
143
|
+
|
|
144
|
+
return config
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def datetime_isoformat(arg: str) -> datetime:
|
|
148
|
+
"""Convert isoformat cli argument to datetime.
|
|
149
|
+
|
|
150
|
+
Arguments:
|
|
151
|
+
arg: cli argument in ISO format
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ArgumentTypeError: in case the parsing failed
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Parsed datetime instance.
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
dt = datetime.fromisoformat(str(arg))
|
|
161
|
+
except ValueError:
|
|
162
|
+
raise ArgumentTypeError(f"invalid datetime value (expected ISO 8601 format): '{arg}'") from None
|
|
163
|
+
|
|
164
|
+
if dt.tzinfo is None:
|
|
165
|
+
dt = _local_timezone.localize(dt)
|
|
166
|
+
return dt
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def regex_type(arg: str) -> str:
|
|
170
|
+
"""Check if a string is a valid RegEx.
|
|
171
|
+
|
|
172
|
+
Arguments:
|
|
173
|
+
arg: cli argument to be checked
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ArgumentTypeError: in case the parsing failed
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
unmodified string argument
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
re.compile(arg)
|
|
183
|
+
except re.error as e:
|
|
184
|
+
raise ArgumentTypeError(f"invalid RegEx value '{arg}': {e}") from None
|
|
185
|
+
return arg
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _validate_config(config: dict) -> None:
|
|
189
|
+
"""Validate the configuration.
|
|
190
|
+
|
|
191
|
+
Arguments:
|
|
192
|
+
config: Parsed configuration hierarchy.
|
|
193
|
+
"""
|
|
194
|
+
found_config_issues = []
|
|
195
|
+
|
|
196
|
+
if config.filter.start_date > config.filter.end_date:
|
|
197
|
+
found_config_issues.append(
|
|
198
|
+
"filter.end-date must be after filter.start-state"
|
|
199
|
+
+ f" (configured: {config.filter.start_date} -> {config.filter.end_date})"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Finally report all found issues
|
|
203
|
+
if found_config_issues:
|
|
204
|
+
print("ERROR: invalid configuration / parameters:", file=sys.stderr)
|
|
205
|
+
for found_config_issue in found_config_issues:
|
|
206
|
+
print(f"- {found_config_issue}", file=sys.stderr)
|
|
207
|
+
sys.exit(1)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Argument parsing."""
|
|
2
|
+
|
|
3
|
+
# ---- Imports ----
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import pytz
|
|
8
|
+
from jsonargparse import ArgumentParser, DefaultHelpFormatter
|
|
9
|
+
from pydantic import SecretStr
|
|
10
|
+
from rich_argparse import RawTextRichHelpFormatter
|
|
11
|
+
from tzlocal import get_localzone
|
|
12
|
+
|
|
13
|
+
from .output import OutputFormat
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---- CommandLine parser ----------------------------------------------------------------------------------------------
|
|
17
|
+
class E3DCCliHelpFormatter(DefaultHelpFormatter, RawTextRichHelpFormatter):
|
|
18
|
+
"""Custom CLI help formatter: Combined DefaultHelpFormatter and RichHelpFormatter."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_config(prog: str, version: str, copy_right: str, author: str, arg_list: Optional[list[str]] = None) -> dict:
|
|
22
|
+
"""Parse the configuration from CLI and/or configuration JSON file.
|
|
23
|
+
|
|
24
|
+
Arguments:
|
|
25
|
+
prog: Program name.
|
|
26
|
+
version: Program version.
|
|
27
|
+
copy_right: Copyright info.
|
|
28
|
+
author: Author info.
|
|
29
|
+
arg_list: Optional command line arguments list.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Dict: Parsed configuration options.
|
|
33
|
+
"""
|
|
34
|
+
argparser = ArgumentParser(
|
|
35
|
+
prog=prog,
|
|
36
|
+
description="Command-line tool to read events from a iCalendar (ICS) files."
|
|
37
|
+
+ f" | Version {version} | {copy_right}",
|
|
38
|
+
version=f"| Version {version}\n{copy_right} {author}",
|
|
39
|
+
default_config_files=["./config.json"],
|
|
40
|
+
print_config=None,
|
|
41
|
+
env_prefix="ICALENDAR_EVENTS_CLI",
|
|
42
|
+
default_env=False,
|
|
43
|
+
formatter_class=E3DCCliHelpFormatter,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
argparser.add_argument("-c", "--config", action="config", help="""Path to JSON configuration file.""")
|
|
47
|
+
|
|
48
|
+
# ---- Calendar URL / access ----
|
|
49
|
+
<<<<<<< HEAD
|
|
50
|
+
argparser.add_argument("--calendar.url", type=str, help="URL of the iCalendar (ICS)")
|
|
51
|
+
=======
|
|
52
|
+
argparser.add_argument(
|
|
53
|
+
"--calendar.url",
|
|
54
|
+
type=str,
|
|
55
|
+
help="""URL of the iCalendar (ICS).
|
|
56
|
+
Also URLs to local files with schema file://<absolute path to local file> are supported.""",
|
|
57
|
+
)
|
|
58
|
+
>>>>>>> 3b6d07f (Use jsonargparse instead argparse)
|
|
59
|
+
argparser.add_argument(
|
|
60
|
+
"--calendar.verify-url",
|
|
61
|
+
type=bool,
|
|
62
|
+
default=True,
|
|
63
|
+
help="Configure SSL verification of the URL",
|
|
64
|
+
)
|
|
65
|
+
argparser.add_argument(
|
|
66
|
+
"--calendar.user",
|
|
67
|
+
type=SecretStr,
|
|
68
|
+
help="Username for calendar URL HTTP authentication (basic authentication)",
|
|
69
|
+
)
|
|
70
|
+
argparser.add_argument(
|
|
71
|
+
"--calendar.password",
|
|
72
|
+
type=SecretStr,
|
|
73
|
+
help="Password for calendar URL HTTP authentication (basic authentication)",
|
|
74
|
+
)
|
|
75
|
+
argparser.add_argument("--calendar.encoding", default="UTF-8", help="Encoding of the calendar")
|
|
76
|
+
|
|
77
|
+
# ---- Filtering ----
|
|
78
|
+
argparser.add_argument(
|
|
79
|
+
"-f",
|
|
80
|
+
"--filter.summary",
|
|
81
|
+
default=".*",
|
|
82
|
+
help="RegEx to filter calendar events based on summary field.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
local_timezone = pytz.timezone(get_localzone().key)
|
|
86
|
+
argparser.add_argument(
|
|
87
|
+
"-s",
|
|
88
|
+
"--filter.start-date",
|
|
89
|
+
type=_datetime_fromisoformat,
|
|
90
|
+
default=local_timezone.localize(datetime.now().replace(microsecond=0)).replace(microsecond=0),
|
|
91
|
+
help="Start date/time of event filter by time (ISO format). Default: now",
|
|
92
|
+
)
|
|
93
|
+
argparser.add_argument(
|
|
94
|
+
"-e",
|
|
95
|
+
"--filter.end-date",
|
|
96
|
+
type=_datetime_fromisoformat,
|
|
97
|
+
default=local_timezone.localize(datetime.combine(datetime.now(), datetime.max.time())).replace(microsecond=0),
|
|
98
|
+
help="End date/time of event filter by time (ISO format). Default: end of today",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# ---- Output ----
|
|
102
|
+
argparser.add_argument(
|
|
103
|
+
"--output.format",
|
|
104
|
+
default=OutputFormat.human_readable,
|
|
105
|
+
type=OutputFormat,
|
|
106
|
+
help="Output format.",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
argparser.add_argument(
|
|
110
|
+
"-o",
|
|
111
|
+
"--output.file",
|
|
112
|
+
type=Optional[str],
|
|
113
|
+
help="Path of JSON output file. If not set the output is written to console / stdout",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# ---- Finally parse the inputs ----
|
|
117
|
+
args = argparser.parse_args(args=arg_list)
|
|
118
|
+
|
|
119
|
+
return args
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _datetime_fromisoformat(arg: str) -> datetime:
|
|
123
|
+
"""Convert isoformat cli argument to datetime.
|
|
124
|
+
|
|
125
|
+
Arguments:
|
|
126
|
+
arg: cli argument in ISO format
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Parsed datetime instance.
|
|
130
|
+
"""
|
|
131
|
+
return datetime.fromisoformat(str(arg))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""ICS calender file downloader."""
|
|
2
|
+
|
|
3
|
+
# ---- Imports ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from requests_file import FileAdapter
|
|
8
|
+
|
|
9
|
+
# ---- Functions -------------------------------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def download_ics(calendard_config: dict) -> str:
|
|
13
|
+
"""Download ICS calendar file from URL.
|
|
14
|
+
|
|
15
|
+
Arguments:
|
|
16
|
+
calendard_config: Calendar configuration hierarchy.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: Downloaded file content.
|
|
20
|
+
"""
|
|
21
|
+
session = requests.Session()
|
|
22
|
+
session.mount("file://", FileAdapter())
|
|
23
|
+
|
|
24
|
+
if calendard_config.user is not None and calendard_config.password is not None:
|
|
25
|
+
session.auth = (calendard_config.user.get_secret_value(), calendard_config.password.get_secret_value())
|
|
26
|
+
response = session.get(url=calendard_config.url, verify=calendard_config.verify_url)
|
|
27
|
+
if response.status_code != 200:
|
|
28
|
+
print(
|
|
29
|
+
f"ERROR: Failed to download ical contents from URL '{response.url}'. "
|
|
30
|
+
+ f"Response status: {response.reason} (status {response.status_code})",
|
|
31
|
+
)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
return response.text
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""ICS calender file downloader."""
|
|
2
|
+
|
|
3
|
+
# ---- Imports ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from requests_file import FileAdapter
|
|
8
|
+
|
|
9
|
+
# ---- Functions -------------------------------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def download_ics(calendard_config: dict) -> str:
|
|
13
|
+
"""Download ICS calendar file from URL.
|
|
14
|
+
|
|
15
|
+
Arguments:
|
|
16
|
+
calendard_config: Calendar configuration hierarchy.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: Downloaded file content.
|
|
20
|
+
"""
|
|
21
|
+
session = requests.Session()
|
|
22
|
+
<<<<<<< HEAD
|
|
23
|
+
=======
|
|
24
|
+
session.mount("file://", FileAdapter())
|
|
25
|
+
|
|
26
|
+
>>>>>>> 3b6d07f (Use jsonargparse instead argparse)
|
|
27
|
+
if calendard_config.user is not None and calendard_config.password is not None:
|
|
28
|
+
session.auth = (calendard_config.user.get_secret_value(), calendard_config.password.get_secret_value())
|
|
29
|
+
response = session.get(url=calendard_config.url, verify=calendard_config.verify_url)
|
|
30
|
+
if response.status_code != 200:
|
|
31
|
+
print(
|
|
32
|
+
f"ERROR: Failed to download ical contents from URL '{response.url}'. "
|
|
33
|
+
+ f"Response status: {response.reason} (status {response.status_code})",
|
|
34
|
+
)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
return response.text
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Access to icalendar objects and hierarchies."""
|
|
2
|
+
|
|
3
|
+
# ---- Imports ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
import re
|
|
5
|
+
from datetime import date, datetime, timedelta
|
|
6
|
+
|
|
7
|
+
import icalendar
|
|
8
|
+
import pytz
|
|
9
|
+
import recurring_ical_events
|
|
10
|
+
from icalendar.cal import Event
|
|
11
|
+
|
|
12
|
+
# https://urllib3.readthedocs.io/en/stable/user-guide.html
|
|
13
|
+
from recurring_ical_events import CalendarQuery
|
|
14
|
+
from tzlocal import get_localzone
|
|
15
|
+
|
|
16
|
+
# ---- Globals ---------------------------------------------------------------------------------------------------------
|
|
17
|
+
__local_timezone = pytz.timezone(get_localzone().key)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---- Functions -------------------------------------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_calendar(calendar_ics: str, filter_config: dict) -> CalendarQuery:
|
|
24
|
+
"""Parse the calendar.
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
calendar_ics: Calendar RAW content string.
|
|
28
|
+
filter_config: Filter configuration hierarchy.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
CalendarQuery: Parsed CalendarQuery.
|
|
32
|
+
"""
|
|
33
|
+
calendar = icalendar.Calendar.from_ical(calendar_ics)
|
|
34
|
+
calendar_components = ["VEVENT"] # Only events
|
|
35
|
+
return recurring_ical_events.of(calendar, components=calendar_components).between(
|
|
36
|
+
filter_config.start_date, filter_config.end_date
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def filter_events(events: CalendarQuery, filter_config: dict, encoding: str) -> CalendarQuery:
|
|
41
|
+
"""Filter the calendar.
|
|
42
|
+
|
|
43
|
+
Arguments:
|
|
44
|
+
events: Calendar to be filtered.
|
|
45
|
+
filter_config: Filter config hierarchy
|
|
46
|
+
encoding: Summary attribute encoding
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
CalendarQuery: Filtered calendar.
|
|
50
|
+
"""
|
|
51
|
+
if filter_config.summary is not None:
|
|
52
|
+
events = filter(
|
|
53
|
+
lambda event: (summary := get_event_summary(event, encoding)) is not None
|
|
54
|
+
and re.match(filter_config.summary, summary) is not None,
|
|
55
|
+
events,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if filter_config.description is not None:
|
|
59
|
+
events = filter(
|
|
60
|
+
lambda event: (description := get_event_description(event, encoding)) is not None
|
|
61
|
+
and re.match(filter_config.description, description) is not None,
|
|
62
|
+
events,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if filter_config.location is not None:
|
|
66
|
+
events = filter(
|
|
67
|
+
lambda event: (location := get_event_location(event, encoding)) is not None
|
|
68
|
+
and re.match(filter_config.location, location) is not None,
|
|
69
|
+
events,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return events
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_event_string_attribute(event: Event, attribute_name: str, encoding: str) -> str:
|
|
76
|
+
"""Get any string attribute from event.
|
|
77
|
+
|
|
78
|
+
Arguments:
|
|
79
|
+
event: Calendar Event.
|
|
80
|
+
attribute_name: Attribute name to be accessed.
|
|
81
|
+
encoding: Calender encoding.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Decoded attribute value.
|
|
85
|
+
"""
|
|
86
|
+
attribute = event.decoded(attribute_name, default=None)
|
|
87
|
+
if attribute is not None:
|
|
88
|
+
attribute = attribute.decode(encoding)
|
|
89
|
+
return attribute
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_event_summary(event: Event, encoding: str) -> str:
|
|
93
|
+
"""Get 'SUMMARY' attribute of calendar event.
|
|
94
|
+
|
|
95
|
+
Arguments:
|
|
96
|
+
event: Calendar Event.
|
|
97
|
+
encoding: Calendar encoding
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Summary attribute.
|
|
101
|
+
"""
|
|
102
|
+
return get_event_string_attribute(event, "SUMMARY", encoding)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_event_description(event: Event, encoding: str) -> str:
|
|
106
|
+
"""Get 'DESCRIPTION' attribute of calendar event.
|
|
107
|
+
|
|
108
|
+
Arguments:
|
|
109
|
+
event: Calendar Event.
|
|
110
|
+
encoding: Calendar encoding
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Description attribute.
|
|
114
|
+
"""
|
|
115
|
+
return get_event_string_attribute(event, "DESCRIPTION", encoding)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_event_location(event: Event, encoding: str) -> str:
|
|
119
|
+
"""Get 'LOCATION' attribute of calendar event.
|
|
120
|
+
|
|
121
|
+
Arguments:
|
|
122
|
+
event: Calendar Event.
|
|
123
|
+
encoding: Calendar encoding
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Location attribute.
|
|
127
|
+
"""
|
|
128
|
+
return get_event_string_attribute(event, "LOCATION", encoding)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_event_dtstart(event: Event) -> date:
|
|
132
|
+
"""Get 'DTSTART' start-date of calendar event.
|
|
133
|
+
|
|
134
|
+
Arguments:
|
|
135
|
+
event: Calendar Event.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Start Date.
|
|
139
|
+
"""
|
|
140
|
+
start = event.decoded("DTSTART")
|
|
141
|
+
if isinstance(start, date) and not isinstance(start, datetime):
|
|
142
|
+
# Convert full-day event to datetime
|
|
143
|
+
start = datetime.combine(start, datetime.min.time())
|
|
144
|
+
start = __local_timezone.localize(start)
|
|
145
|
+
return start
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_event_dtend(event: Event) -> date:
|
|
149
|
+
"""Get 'DTEND' end-date of calendar event.
|
|
150
|
+
|
|
151
|
+
Arguments:
|
|
152
|
+
event: Calendar Event.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
End Date.
|
|
156
|
+
"""
|
|
157
|
+
end = event.decoded("DTEND")
|
|
158
|
+
if end.resolution == timedelta(days=1):
|
|
159
|
+
# For full-day events the DTEND is always one day after DTSTART.
|
|
160
|
+
# Therefore subtract 1 day and then set time to end of day
|
|
161
|
+
end -= timedelta(days=1)
|
|
162
|
+
end = datetime.combine(end, datetime.max.time()).replace(microsecond=0)
|
|
163
|
+
end = __local_timezone.localize(end)
|
|
164
|
+
return end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Handling of different output target and formats."""
|
|
2
|
+
|
|
3
|
+
# ---- Imports ---------------------------------------------------------------------------------------------------------
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from recurring_ical_events import CalendarQuery
|
|
10
|
+
|
|
11
|
+
from .icalendar import get_event_description, get_event_dtend, get_event_dtstart, get_event_location, get_event_summary
|
|
12
|
+
|
|
13
|
+
# ---- Functions -------------------------------------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OutputFormat(Enum):
|
|
17
|
+
"""All possible output formats."""
|
|
18
|
+
|
|
19
|
+
human_readable = "human_readable" # pylint: disable=invalid-name;reason=camel_case style wanted for cli param
|
|
20
|
+
json = "json" # pylint: disable=invalid-name;reason=camel_case style wanted for cli param
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def output_events(events: CalendarQuery, config: dict) -> None:
|
|
24
|
+
"""Output the calendar.
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
events: Calendar events.
|
|
28
|
+
config: Configuration hierarchy.
|
|
29
|
+
"""
|
|
30
|
+
sorted_events = _sort_events(events)
|
|
31
|
+
|
|
32
|
+
if config.output.format == OutputFormat.json:
|
|
33
|
+
output_json(sorted_events, config)
|
|
34
|
+
else:
|
|
35
|
+
output_human_readable(sorted_events, config)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def output_json(events: CalendarQuery, config: dict) -> None:
|
|
39
|
+
"""Output the events in JSON format.
|
|
40
|
+
|
|
41
|
+
Arguments:
|
|
42
|
+
events: Calendar events.
|
|
43
|
+
config: Configuration hierarchy.
|
|
44
|
+
"""
|
|
45
|
+
filters = {"start-date": config.filter.start_date.isoformat(), "end-date": config.filter.end_date.isoformat()}
|
|
46
|
+
if config.filter.summary:
|
|
47
|
+
filters["summary"] = config.filter.summary
|
|
48
|
+
if config.filter.description:
|
|
49
|
+
filters["description"] = config.filter.description
|
|
50
|
+
if config.filter.location:
|
|
51
|
+
filters["location"] = config.filter.location
|
|
52
|
+
|
|
53
|
+
# Detailed Events List
|
|
54
|
+
events_output = []
|
|
55
|
+
for event in events:
|
|
56
|
+
event_output = {
|
|
57
|
+
"start-date": get_event_dtstart(event).isoformat(),
|
|
58
|
+
"end-date": get_event_dtend(event).isoformat(),
|
|
59
|
+
"summary": get_event_summary(event, config.calendar.encoding),
|
|
60
|
+
}
|
|
61
|
+
description = get_event_description(event, config.calendar.encoding)
|
|
62
|
+
if description is not None:
|
|
63
|
+
event_output["description"] = description
|
|
64
|
+
|
|
65
|
+
location = get_event_location(event, config.calendar.encoding)
|
|
66
|
+
if location is not None:
|
|
67
|
+
event_output["location"] = location
|
|
68
|
+
events_output.append(event_output)
|
|
69
|
+
|
|
70
|
+
json_hierarchy = {"filter": filters, "events": events_output}
|
|
71
|
+
|
|
72
|
+
# Finally output the JSON hierarchy to stdout or the configured file
|
|
73
|
+
if config.output.file is None:
|
|
74
|
+
json.dump(json_hierarchy, fp=sys.stdout, indent=2, ensure_ascii=False)
|
|
75
|
+
else:
|
|
76
|
+
with open(config.output.file, "w", encoding="utf-8") as file:
|
|
77
|
+
json.dump(json_hierarchy, fp=file, indent=2, ensure_ascii=False)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def output_human_readable(events: CalendarQuery, config: dict) -> None:
|
|
81
|
+
"""Output the events in human readable format.
|
|
82
|
+
|
|
83
|
+
Arguments:
|
|
84
|
+
events: Calendar events.
|
|
85
|
+
config: Configuration hierarchy.
|
|
86
|
+
"""
|
|
87
|
+
output = []
|
|
88
|
+
|
|
89
|
+
output.append(f"Start Date: {config.filter.start_date.isoformat()}")
|
|
90
|
+
output.append(f"End Date: {config.filter.end_date.isoformat()}")
|
|
91
|
+
if config.filter.summary:
|
|
92
|
+
output.append(f"Summary Filter: {config.filter.summary}")
|
|
93
|
+
if config.filter.description:
|
|
94
|
+
output.append(f"Description Filter: {config.filter.description}")
|
|
95
|
+
if config.filter.location:
|
|
96
|
+
output.append(f"Location Filter: {config.filter.location}")
|
|
97
|
+
output.append(f"Number of Events: {len(events)}{os.linesep}")
|
|
98
|
+
|
|
99
|
+
for event in events:
|
|
100
|
+
start = get_event_dtstart(event)
|
|
101
|
+
end = get_event_dtend(event)
|
|
102
|
+
summary = get_event_summary(event, config.calendar.encoding)
|
|
103
|
+
description = get_event_description(event, config.calendar.encoding)
|
|
104
|
+
location = get_event_location(event, config.calendar.encoding)
|
|
105
|
+
|
|
106
|
+
duration = end - start
|
|
107
|
+
start_end_string = f"{start.isoformat()} -> {end.isoformat()} [{duration.total_seconds():.0f} sec]"
|
|
108
|
+
opt_description_string = f" | Description: {description}" if description is not None else ""
|
|
109
|
+
opt_location_string = f" | Location: {description}" if location is not None else ""
|
|
110
|
+
|
|
111
|
+
output.append(f"{start_end_string: <70} | {summary}{opt_description_string}{opt_location_string}")
|
|
112
|
+
|
|
113
|
+
# build final output string incl. line separators
|
|
114
|
+
output = os.linesep.join(output)
|
|
115
|
+
|
|
116
|
+
# Finally output to stdout or the configured file
|
|
117
|
+
if config.output.file is None:
|
|
118
|
+
print(output)
|
|
119
|
+
else:
|
|
120
|
+
with open(config.output.file, "w", encoding="utf-8") as file:
|
|
121
|
+
file.write(output)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _sort_events(events: CalendarQuery) -> CalendarQuery:
|
|
125
|
+
"""Sort calendar.
|
|
126
|
+
|
|
127
|
+
Arguments:
|
|
128
|
+
events: Calendar to be sorted.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
CalendarQuery: Sorted calendar.
|
|
132
|
+
"""
|
|
133
|
+
return sorted(events, key=get_event_dtstart, reverse=False)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: icalendar-events-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Command-line tool to read events from a iCalendar (ICS)
|
|
5
|
+
Author-Email: Sebastian Waldvogel <sebastian@waldvogels.de>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/waldbaer/icalendar-events-cli
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: tzlocal==5.2
|
|
10
|
+
Requires-Dist: pytz==2024.2
|
|
11
|
+
Requires-Dist: recurring-ical-events==3.4.1
|
|
12
|
+
Requires-Dist: requests>=2.32.3
|
|
13
|
+
Requires-Dist: requests-file>=2.1.0
|
|
14
|
+
Requires-Dist: jsonargparse>=4.36.0
|
|
15
|
+
Requires-Dist: rich-argparse>=1.6.0
|
|
16
|
+
Requires-Dist: pydantic>=2.10.5
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
[](https://badge.fury.io/py/icalendar-events-cli)
|
|
20
|
+
[](https://opensource.org/licenses/MIT)
|
|
21
|
+
[](https://github.com/waldbaer/icalendar-events-cli/issues)
|
|
22
|
+
[](https://github.com/waldbaer/icalendar-events-cli/actions/workflows/python-pdm.yml)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Command-line tool to read icalendar events
|
|
26
|
+
|
|
27
|
+
## Introduction
|
|
28
|
+
|
|
29
|
+
This command-line tool allows users to query and filter [iCalendar (RFC 5545)](https://icalendar.org/RFC-Specifications/iCalendar-RFC-5545/) calendars. It leverages the excellent [recurring-ical-events](https://github.com/niccokunzmann/python-recurring-ical-events) library for parsing and querying the calendar contents.
|
|
30
|
+
|
|
31
|
+
Leveraging the powerful [jsonargparse](https://jsonargparse.readthedocs.io/) library, this tool supports configuration and control via command-line parameters or a JSON configuration file.
|
|
32
|
+
|
|
33
|
+
## Features ##
|
|
34
|
+
- Download and parse iCalendar files
|
|
35
|
+
- from remote HTTP URL (`https://<path to icalendar server>`)
|
|
36
|
+
- from local file URL (`file://<abs. path to local ICS file>`)
|
|
37
|
+
- configurable encoding
|
|
38
|
+
- Filtering
|
|
39
|
+
- by start- and end-date range
|
|
40
|
+
- by event summary, description or location text (RegEx match)
|
|
41
|
+
- Different Outputs
|
|
42
|
+
- Formats: JSON, human-readable (pretty printed)
|
|
43
|
+
- Targets: shell (stdout), file
|
|
44
|
+
|
|
45
|
+
## Changelog
|
|
46
|
+
Changes can be followed at [CHANGELOG.md](https://github.com/waldbaer/icalendar-events-cli/blob/master/CHANGELOG.md).
|
|
47
|
+
|
|
48
|
+
## Requirements ##
|
|
49
|
+
|
|
50
|
+
- [Python 3.9](https://www.python.org/)
|
|
51
|
+
- [pip](https://pip.pypa.io/) or [pipx](https://pipx.pypa.io/stable/)
|
|
52
|
+
|
|
53
|
+
For development:
|
|
54
|
+
- [python-pdm (package dependency manager)](https://pdm-project.org/)
|
|
55
|
+
|
|
56
|
+
## Setup
|
|
57
|
+
|
|
58
|
+
### With pip / pipx
|
|
59
|
+
```
|
|
60
|
+
pip install icalendar-events-cli
|
|
61
|
+
pipx install icalendar-events-cli
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Setup directly from github repo / clone
|
|
65
|
+
```
|
|
66
|
+
git clone git@github.com:waldbaer/icalendar-events-cli.git
|
|
67
|
+
cd icalendar-events-cli
|
|
68
|
+
|
|
69
|
+
python -m venv .venv
|
|
70
|
+
source ./.venv/bin/activate
|
|
71
|
+
pip install .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
All parameters can be provided either as command-line arguments or through a JSON configuration file (default: `config.json`).
|
|
77
|
+
A combination of both methods is also supported.
|
|
78
|
+
|
|
79
|
+
A common approach is to define the calendar URL and HTTP authentication credentials in the JSON configuration file,
|
|
80
|
+
while specifying filters as command-line arguments.
|
|
81
|
+
Alternatively, you can define all credentials via command-line parameters or include the applied filters directly in the
|
|
82
|
+
JSON configuration file.
|
|
83
|
+
|
|
84
|
+
The results of all executed queries are returned in human-readable (pretty-printed) or
|
|
85
|
+
machine-readable JSON format.
|
|
86
|
+
This output can be displayed directly on the shell (stdout) or saved to a file.
|
|
87
|
+
|
|
88
|
+
The machine-readable JSON output format is designed for seamless integration with automation
|
|
89
|
+
platforms, such as [Node-RED](https://nodered.org/), which typically execute the
|
|
90
|
+
`icalendar-events-cli` tool.
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
### Examples
|
|
95
|
+
|
|
96
|
+
#### Example 1: Query Public Holiday Calendar
|
|
97
|
+
|
|
98
|
+
- Use human-readable output format
|
|
99
|
+
- Pass all parameters as command-line arguments
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
icalendar-events-cli --calendar.url https://www.thunderbird.net/media/caldata/autogen/GermanHolidays.ics \
|
|
103
|
+
--filter.start-date $(date +%Y)-01-01T02:00:00+02:00 \
|
|
104
|
+
--filter.end-date $(date +%Y)-12-31T02:00:00+01:00 \
|
|
105
|
+
--filter.summary ".*(Weihnacht|Oster).*"
|
|
106
|
+
|
|
107
|
+
Start Date: 2025-01-01T02:00:00+02:00
|
|
108
|
+
End Date: 2025-12-31T02:00:00+01:00
|
|
109
|
+
Summary Filter: .*(Weihnacht|Oster).*
|
|
110
|
+
Number of Events: 3
|
|
111
|
+
|
|
112
|
+
2025-04-20T00:00:00+02:00 -> 2025-04-20T23:59:59+02:00 [86399 sec] | Ostersonntag (Brandenburg) | Description: Common local holiday - Der Ostersonntag ist laut der christlichen Bibel ein Feiertag in Deutschland, um die Auferstehung Jesu Christi zu feiern.
|
|
113
|
+
2025-04-21T00:00:00+02:00 -> 2025-04-21T23:59:59+02:00 [86399 sec] | Ostermontag | Description: Christian - Viele Menschen in Deutschland begehen jährlich den Ostermontag am Tag nach dem Ostersonntag. Es ist in allen Bundesstaaten ein Feiertag.
|
|
114
|
+
2025-12-25T00:00:00+01:00 -> 2025-12-25T23:59:59+01:00 [86399 sec] | Weihnachten | Description: Christian - Der Weihnachtstag markiert die Geburt Jesu Christi und ist ein gesetzlicher Feiertag in Deutschland. Es ist jedes Jahr am 25. Dezember.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### Example 2: Query School Vacation Calendar
|
|
118
|
+
|
|
119
|
+
The machine-readable JSON output format is designed for seamless integration with automation platforms, such as [Node-RED](https://nodered.org/), which typically execute the `icalendar-events-cli` tool.
|
|
120
|
+
|
|
121
|
+
- Use JSON output format++
|
|
122
|
+
- Mixed parameter configuration: Pass only end-date as command-line argument.
|
|
123
|
+
|
|
124
|
+
Create `school-summer-vacation.json` containing calendar URL, summary filter and output format settings:
|
|
125
|
+
```
|
|
126
|
+
{
|
|
127
|
+
"calendar" : {
|
|
128
|
+
"url" : "https://www.feiertage-deutschland.de/kalender-download/ics/schulferien-baden-wuerttemberg.ics",
|
|
129
|
+
"verify_url": true
|
|
130
|
+
},
|
|
131
|
+
"filter": {
|
|
132
|
+
"summary": "Sommer.*"
|
|
133
|
+
},
|
|
134
|
+
"output": {
|
|
135
|
+
"format": "json"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Query the calendar for summer vacation until end of next year:
|
|
141
|
+
```
|
|
142
|
+
icalendar-events-cli --config school-summer-vacation.json --filter.end-date $(($(date +%Y) + 1))-12-31T23:59:59
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
"filter": {
|
|
146
|
+
"start-date": "2025-01-25T11:09:20+01:00",
|
|
147
|
+
"end-date": "2026-12-31T23:59:59+01:00",
|
|
148
|
+
"summary": "Sommer.*"
|
|
149
|
+
},
|
|
150
|
+
"events": [
|
|
151
|
+
{
|
|
152
|
+
"start-date": "2025-07-31T00:00:00+02:00",
|
|
153
|
+
"end-date": "2025-09-13T23:59:59+02:00",
|
|
154
|
+
"summary": "Sommerferien Baden-Württemberg 2025",
|
|
155
|
+
"description": "Schulferien 2025: https://www.feiertage-deutschland.de/schulferien/2025/",
|
|
156
|
+
"location": "BW"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"start-date": "2026-07-30T00:00:00+02:00",
|
|
160
|
+
"end-date": "2026-09-12T23:59:59+02:00",
|
|
161
|
+
"summary": "Sommerferien Baden-Württemberg 2026",
|
|
162
|
+
"description": "Schulferien 2026: https://www.feiertage-deutschland.de/schulferien/2026/",
|
|
163
|
+
"location": "BW"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
### All Available Parameters and Configuration Options
|
|
171
|
+
|
|
172
|
+
Details about all available options:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
Usage: icalendar-events-cli [-h] [--version] [-c CONFIG] --calendar.url URL [--calendar.verify-url {true,false}]
|
|
176
|
+
[--calendar.user USER] [--calendar.password PASSWORD] [--calendar.encoding ENCODING]
|
|
177
|
+
[-s START_DATE] [-e END_DATE] [-f SUMMARY] [--filter.description DESCRIPTION]
|
|
178
|
+
[--filter.location LOCATION] [--output.format {human_readable,json}] [-o FILE]
|
|
179
|
+
|
|
180
|
+
Command-line tool to read events from a iCalendar (ICS) files. | Version 1.0.0 | Copyright 2023-2025
|
|
181
|
+
|
|
182
|
+
Default Config File Locations:
|
|
183
|
+
['./config.json'], Note: no existing default config file found.
|
|
184
|
+
|
|
185
|
+
Options:
|
|
186
|
+
-h, --help Show this help message and exit.
|
|
187
|
+
--version Print version and exit.
|
|
188
|
+
-c, --config CONFIG Path to JSON configuration file.
|
|
189
|
+
--calendar.url URL URL of the iCalendar (ICS).
|
|
190
|
+
Also URLs to local files with schema file://<absolute path to local file> are supported. (required, type: None)
|
|
191
|
+
--calendar.verify-url {true,false}
|
|
192
|
+
Configure SSL verification of the URL (type: None, default: True)
|
|
193
|
+
--calendar.user USER Username for calendar URL HTTP authentication (basic authentication) (type: None, default: None)
|
|
194
|
+
--calendar.password PASSWORD
|
|
195
|
+
Password for calendar URL HTTP authentication (basic authentication) (type: None, default: None)
|
|
196
|
+
--calendar.encoding ENCODING
|
|
197
|
+
Encoding of the calendar (default: UTF-8)
|
|
198
|
+
-s, --filter.start-date START_DATE
|
|
199
|
+
Start date/time of event filter by time (ISO format). Default: now (type: datetime_isoformat, default: now)
|
|
200
|
+
-e, --filter.end-date END_DATE
|
|
201
|
+
End date/time of event filter by time (ISO format). Default: end of today (type: datetime_isoformat, default: end of today)
|
|
202
|
+
-f, --filter.summary SUMMARY
|
|
203
|
+
RegEx to filter calendar events based on the summary attribute. (type: regex_type, default: None)
|
|
204
|
+
--filter.description DESCRIPTION
|
|
205
|
+
RegEx to filter calendar events based on the description attribute. (type: regex_type, default: None)
|
|
206
|
+
--filter.location LOCATION
|
|
207
|
+
RegEx to filter calendar events based on the location attribute. (type: regex_type, default: None)
|
|
208
|
+
--output.format {human_readable,json}
|
|
209
|
+
Output format. (type: None, default: human_readable)
|
|
210
|
+
-o, --output.file FILE
|
|
211
|
+
Path of JSON output file. If not set the output is written to console / stdout (type: None, default: None)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
### Setup environment
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
pdm install --dev
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Format / Linter / Tests
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
# Check code style
|
|
227
|
+
pdm run format
|
|
228
|
+
|
|
229
|
+
# Check linter
|
|
230
|
+
pdm run lint
|
|
231
|
+
|
|
232
|
+
# Run tests
|
|
233
|
+
pdm run tests
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Publish
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
# API token will be requested interactively as password
|
|
240
|
+
pdm publish -u __token__
|
|
241
|
+
|
|
242
|
+
# or to test.pypi.org
|
|
243
|
+
pdm publish --repository testpypi -u __token__
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Acknowledgments
|
|
247
|
+
Special thanks to [recurring-ical-events](https://github.com/niccokunzmann/python-recurring-ical-events) for providing
|
|
248
|
+
the core library that powers this tool.
|
|
249
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
icalendar_events_cli-1.0.0.dist-info/METADATA,sha256=fmfMuo8IGNHOaw7SoxxylekqN5bzKyDheXV581PMWsw,10023
|
|
2
|
+
icalendar_events_cli-1.0.0.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
|
3
|
+
icalendar_events_cli-1.0.0.dist-info/entry_points.txt,sha256=R1ZEdlwKR2jVWZUrZd6Ow3Z06GuXEHn3DNFi0t-fJlo,91
|
|
4
|
+
icalendar_events_cli-1.0.0.dist-info/licenses/LICENSE,sha256=VMNRqNjjlAj58av8QYDG29jshQvvIM8YfpS50NBcBPI,1085
|
|
5
|
+
icalendar_events_cli/__init__.py,sha256=fUje7vgFT24oJYk375-geUaz4RIVixNYE5PqOL9VmUo,19
|
|
6
|
+
icalendar_events_cli/__main__.py,sha256=6X7Faf6vK7DovEfpKiQNXvCTRjWEie8S5AB22dUbzhI,2040
|
|
7
|
+
icalendar_events_cli/argparse.py,sha256=8B56Vcfnyi066w4gfb2fPYknEk7LMdwAc2DqV_jFdt8,6299
|
|
8
|
+
icalendar_events_cli/argparse.py.orig,sha256=hOpOKCgf4NL4dwdC6YOVmL1qgtzh86POGpvwBrXaVUs,4285
|
|
9
|
+
icalendar_events_cli/downloader.py,sha256=5fOPyAevWSSZnK4C0w8VqYEjqEuaMZO6_SuDQsTDpNg,1214
|
|
10
|
+
icalendar_events_cli/downloader.py.orig,sha256=IKeUeAsDe7RV3eM7SvsDcxZa9jXLJfPBf4-Pp8U7zSY,1287
|
|
11
|
+
icalendar_events_cli/icalendar.py,sha256=RSEXJQeeolvpGt1BSMomid6ufh6mzYmsMQKqqtex3wc,4927
|
|
12
|
+
icalendar_events_cli/output.py,sha256=u_rgZd1-T83RZQAIJkTAbORI-32Vjpv7a2SAoEP1CjQ,4970
|
|
13
|
+
icalendar_events_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Sebastian Waldvogel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|