ics-query 0.1.1a0__py3-none-any.whl → 0.3.1b0__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.
- ics_query/__init__.py +18 -2
- ics_query/__main__.py +15 -0
- ics_query/_version.py +2 -2
- ics_query/cli.py +455 -23
- ics_query/parse.py +41 -4
- ics_query/query.py +71 -0
- ics_query/tests/conftest.py +15 -0
- ics_query/tests/runs/all --tz Singapore one-event.ics -.run +9 -0
- ics_query/tests/runs/all three-events.ics -.run +33 -0
- ics_query/tests/runs/at 2024-08-20 Berlin-Los-Angeles.ics -.run +23 -0
- ics_query/tests/runs/between 20240823 4d recurring-work-events.ics -.run +24 -0
- ics_query/tests/runs/calendars/Berlin-Los-Angeles.ics +381 -0
- ics_query/tests/runs/calendars/empty-calendar.ics +7 -0
- ics_query/tests/runs/calendars/empty-file.ics +0 -0
- ics_query/tests/runs/calendars/recurring-work-events.ics +223 -0
- ics_query/tests/runs/calendars/simple-journal.ics +15 -0
- ics_query/tests/runs/calendars/simple-todo.ics +15 -0
- ics_query/tests/runs/calendars/three-events.ics +37 -0
- ics_query/tests/runs/first -c VJOURNAL -c VEVENT one-event.ics -.run +9 -0
- ics_query/tests/runs/first -c VJOURNAL one-event.ics -.run +0 -0
- ics_query/tests/runs/first -c VJOURNAL simple-journal.ics -.run +12 -0
- ics_query/tests/runs/first -c VTODO -c VJOURNAL simple-todo.ics -.run +10 -0
- ics_query/tests/runs/first -c VTODO simple-todo.ics -.run +10 -0
- ics_query/tests/runs/first empty-calendar.ics -.run +0 -0
- ics_query/tests/runs/first empty-file.ics -.run +0 -0
- ics_query/tests/runs/first recurring-work-events.ics -.run +12 -0
- ics_query/tests/test_command_line.py +34 -0
- ics_query/tests/test_parse_date.py +23 -5
- ics_query/tests/test_parse_timedelta.py +40 -0
- ics_query/version.py +33 -3
- {ics_query-0.1.1a0.dist-info → ics_query-0.3.1b0.dist-info}/METADATA +247 -51
- ics_query-0.3.1b0.dist-info/RECORD +43 -0
- ics_query-0.1.1a0.dist-info/RECORD +0 -22
- {ics_query-0.1.1a0.dist-info → ics_query-0.3.1b0.dist-info}/WHEEL +0 -0
- {ics_query-0.1.1a0.dist-info → ics_query-0.3.1b0.dist-info}/entry_points.txt +0 -0
- {ics_query-0.1.1a0.dist-info → ics_query-0.3.1b0.dist-info}/licenses/LICENSE +0 -0
ics_query/__init__.py
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
# ics-query
|
|
2
|
+
# Copyright (C) 2024 Nicco Kunzmann
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
from .cli import cli, main
|
|
2
17
|
from .version import __version__, __version_tuple__, version, version_tuple
|
|
3
18
|
|
|
4
19
|
__all__ = [
|
|
5
|
-
"
|
|
20
|
+
"cli",
|
|
6
21
|
"app",
|
|
7
22
|
"__version__",
|
|
8
23
|
"version",
|
|
9
24
|
"__version_tuple__",
|
|
10
25
|
"version_tuple",
|
|
26
|
+
"main",
|
|
11
27
|
]
|
ics_query/__main__.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# ics-query
|
|
2
|
+
# Copyright (C) 2024 Nicco Kunzmann
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
1
16
|
import sys
|
|
2
17
|
|
|
3
18
|
from .cli import main
|
ics_query/_version.py
CHANGED
ics_query/cli.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# ics-query
|
|
2
|
+
# Copyright (C) 2024 Nicco Kunzmann
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
1
16
|
"""The command line interface."""
|
|
2
17
|
|
|
3
18
|
from __future__ import annotations
|
|
@@ -6,24 +21,44 @@ import functools
|
|
|
6
21
|
import os # noqa: TCH003
|
|
7
22
|
import sys
|
|
8
23
|
import typing as t
|
|
24
|
+
import zoneinfo
|
|
9
25
|
|
|
10
26
|
import click
|
|
11
|
-
import recurring_ical_events
|
|
12
27
|
from icalendar.cal import Calendar, Component
|
|
13
|
-
from recurring_ical_events import CalendarQuery
|
|
14
28
|
|
|
15
29
|
from . import parse
|
|
16
|
-
from .
|
|
30
|
+
from .query import Query
|
|
31
|
+
from .version import cli_version
|
|
17
32
|
|
|
18
33
|
if t.TYPE_CHECKING:
|
|
19
34
|
from io import FileIO
|
|
20
35
|
|
|
36
|
+
import recurring_ical_events
|
|
21
37
|
from icalendar.cal import Component
|
|
22
38
|
|
|
23
|
-
from .parse import
|
|
39
|
+
from .parse import Date
|
|
24
40
|
|
|
25
41
|
print = functools.partial(print, file=sys.stderr) # noqa: A001
|
|
26
42
|
|
|
43
|
+
ENV_PREFIX = "ICS_QUERY"
|
|
44
|
+
LICENSE = """
|
|
45
|
+
ics-query
|
|
46
|
+
Copyright (C) 2024 Nicco Kunzmann
|
|
47
|
+
|
|
48
|
+
This program is free software: you can redistribute it and/or modify
|
|
49
|
+
it under the terms of the GNU General Public License as published by
|
|
50
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
51
|
+
(at your option) any later version.
|
|
52
|
+
|
|
53
|
+
This program is distributed in the hope that it will be useful,
|
|
54
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
55
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
56
|
+
GNU General Public License for more details.
|
|
57
|
+
|
|
58
|
+
You should have received a copy of the GNU General Public License
|
|
59
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
60
|
+
"""
|
|
61
|
+
|
|
27
62
|
|
|
28
63
|
class ComponentsResult:
|
|
29
64
|
"""Output interface for components."""
|
|
@@ -36,6 +71,11 @@ class ComponentsResult:
|
|
|
36
71
|
"""Return a component."""
|
|
37
72
|
self._file.write(component.to_ical())
|
|
38
73
|
|
|
74
|
+
def add_components(self, components: t.Iterable[Component]):
|
|
75
|
+
"""Add all components."""
|
|
76
|
+
for component in components:
|
|
77
|
+
self.add_component(component)
|
|
78
|
+
|
|
39
79
|
|
|
40
80
|
class ComponentsResultArgument(click.File):
|
|
41
81
|
"""Argument for the result."""
|
|
@@ -52,15 +92,37 @@ class ComponentsResultArgument(click.File):
|
|
|
52
92
|
|
|
53
93
|
|
|
54
94
|
class JoinedCalendars:
|
|
55
|
-
def __init__(
|
|
95
|
+
def __init__(
|
|
96
|
+
self, calendars: list[Calendar], timezone: str, components: t.Sequence[str]
|
|
97
|
+
):
|
|
56
98
|
"""Join multiple calendars."""
|
|
57
|
-
self.queries = [
|
|
99
|
+
self.queries = [
|
|
100
|
+
Query(calendar, timezone, components=components) for calendar in calendars
|
|
101
|
+
]
|
|
58
102
|
|
|
59
103
|
def at(self, dt: tuple[int]) -> t.Generator[Component]:
|
|
60
104
|
"""Return the components."""
|
|
61
105
|
for query in self.queries:
|
|
62
106
|
yield from query.at(dt)
|
|
63
107
|
|
|
108
|
+
def first(self) -> t.Generator[Component]:
|
|
109
|
+
"""Return the first events of all calendars."""
|
|
110
|
+
for query in self.queries:
|
|
111
|
+
for component in query.all():
|
|
112
|
+
yield component
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
def all(self) -> t.Generator[Component]:
|
|
116
|
+
"""Return the first events of all calendars."""
|
|
117
|
+
for query in self.queries:
|
|
118
|
+
yield from query.all()
|
|
119
|
+
|
|
120
|
+
def between(
|
|
121
|
+
self, start: parse.Date, end: parse.DateAndDelta
|
|
122
|
+
) -> t.Generator[Component]:
|
|
123
|
+
for query in self.queries:
|
|
124
|
+
yield from query.between(start, end)
|
|
125
|
+
|
|
64
126
|
|
|
65
127
|
class CalendarQueryInputArgument(click.File):
|
|
66
128
|
"""Argument for the result."""
|
|
@@ -74,16 +136,105 @@ class CalendarQueryInputArgument(click.File):
|
|
|
74
136
|
"""Return a CalendarQuery."""
|
|
75
137
|
file = super().convert(value, param, ctx)
|
|
76
138
|
calendars = Calendar.from_ical(file.read(), multiple=True)
|
|
77
|
-
|
|
139
|
+
components = ctx.params.get("component", ("VEVENT", "VTODO", "VJOURNAL"))
|
|
140
|
+
timezone = ctx.params.get("tz", "")
|
|
141
|
+
return JoinedCalendars(calendars, timezone, components)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
opt_components = click.option(
|
|
145
|
+
"--component",
|
|
146
|
+
"-c",
|
|
147
|
+
multiple=True,
|
|
148
|
+
envvar=ENV_PREFIX + "_COMPONENT",
|
|
149
|
+
help=(
|
|
150
|
+
"Select the components which can be returned. "
|
|
151
|
+
"By default all supported components can be in the result. "
|
|
152
|
+
"Possible values are: VEVENT, VTODO, VJOURNAL. "
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
opt_timezone = click.option(
|
|
157
|
+
"--tz",
|
|
158
|
+
envvar=ENV_PREFIX + "_TZ",
|
|
159
|
+
help=("Set the timezone. See also --available-timezones"),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def arg_calendar(func):
|
|
164
|
+
"""Decorator for a calendar argument with all used options."""
|
|
165
|
+
arg = click.argument("calendar", type=CalendarQueryInputArgument("rb"))
|
|
166
|
+
|
|
167
|
+
@functools.wraps(func)
|
|
168
|
+
def wrapper(*args, component=(), tz="", **kw): # noqa: ARG001
|
|
169
|
+
"""Remove some parameters."""
|
|
170
|
+
return func(*args, **kw)
|
|
171
|
+
|
|
172
|
+
return opt_timezone(opt_components(arg(wrapper)))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
arg_output = click.argument("output", type=ComponentsResultArgument("wb"), default="-")
|
|
176
|
+
# Option with many values and list as result
|
|
177
|
+
# see https://click.palletsprojects.com/en/latest/options/#multiple-options
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def opt_available_timezones(*param_decls: str, **kwargs: t.Any) -> t.Callable:
|
|
181
|
+
"""List available timezones.
|
|
182
|
+
|
|
183
|
+
This is copied from the --help option.
|
|
184
|
+
"""
|
|
78
185
|
|
|
186
|
+
def callback(ctx: click.Context, param: click.Parameter, value: bool) -> None: # noqa: FBT001, ARG001
|
|
187
|
+
if not value or ctx.resilient_parsing:
|
|
188
|
+
return
|
|
79
189
|
|
|
80
|
-
|
|
81
|
-
|
|
190
|
+
first_zones = ["localtime", "UTC"]
|
|
191
|
+
all_zones = zoneinfo.available_timezones()
|
|
192
|
+
for zone in first_zones:
|
|
193
|
+
if zone in all_zones:
|
|
194
|
+
click.echo(zone)
|
|
195
|
+
for zone in sorted(all_zones, key=str.lower):
|
|
196
|
+
click.echo(zone)
|
|
197
|
+
ctx.exit()
|
|
198
|
+
|
|
199
|
+
if not param_decls:
|
|
200
|
+
param_decls = ("--available-timezones",)
|
|
201
|
+
|
|
202
|
+
kwargs.setdefault("is_flag", True)
|
|
203
|
+
kwargs.setdefault("expose_value", False)
|
|
204
|
+
kwargs.setdefault("is_eager", True)
|
|
205
|
+
kwargs.setdefault("help", "List all available timezones and exit.")
|
|
206
|
+
kwargs["callback"] = callback
|
|
207
|
+
return click.option(*param_decls, **kwargs)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def opt_license(*param_decls: str, **kwargs: t.Any) -> t.Callable:
|
|
211
|
+
"""List available timezones.
|
|
212
|
+
|
|
213
|
+
This is copied from the --help option.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def callback(ctx: click.Context, param: click.Parameter, value: bool) -> None: # noqa: FBT001, ARG001
|
|
217
|
+
if not value or ctx.resilient_parsing:
|
|
218
|
+
return
|
|
219
|
+
click.echo(LICENSE)
|
|
220
|
+
ctx.exit()
|
|
221
|
+
|
|
222
|
+
if not param_decls:
|
|
223
|
+
param_decls = ("--license",)
|
|
224
|
+
|
|
225
|
+
kwargs.setdefault("is_flag", True)
|
|
226
|
+
kwargs.setdefault("expose_value", False)
|
|
227
|
+
kwargs.setdefault("is_eager", True)
|
|
228
|
+
kwargs.setdefault("help", "Show the license and exit.")
|
|
229
|
+
kwargs["callback"] = callback
|
|
230
|
+
return click.option(*param_decls, **kwargs)
|
|
82
231
|
|
|
83
232
|
|
|
84
233
|
@click.group()
|
|
85
|
-
@click.version_option(
|
|
86
|
-
|
|
234
|
+
@click.version_option(cli_version)
|
|
235
|
+
@opt_available_timezones()
|
|
236
|
+
@opt_license()
|
|
237
|
+
def cli():
|
|
87
238
|
"""Find out what happens in ICS calendar files.
|
|
88
239
|
|
|
89
240
|
ics-query can query and filter RFC 5545 compatible .ics files.
|
|
@@ -107,8 +258,8 @@ def main():
|
|
|
107
258
|
If OUTPUT is "-", then the standard output is used.
|
|
108
259
|
|
|
109
260
|
\b
|
|
110
|
-
|
|
111
|
-
|
|
261
|
+
Calculation
|
|
262
|
+
-----------
|
|
112
263
|
|
|
113
264
|
An event can be very long. If you request smaller time spans or a time as
|
|
114
265
|
exact as a second, the event will still occur within this time span if it
|
|
@@ -121,20 +272,72 @@ def main():
|
|
|
121
272
|
The START is INCLUSIVE, then END is EXCLUSIVE.
|
|
122
273
|
|
|
123
274
|
\b
|
|
124
|
-
|
|
125
|
-
|
|
275
|
+
Timezones
|
|
276
|
+
---------
|
|
277
|
+
|
|
278
|
+
We have several timezones available to choose from.
|
|
279
|
+
While the calendar entries might use their own timezone definitions,
|
|
280
|
+
the timezone parameters of ics-query use the timezone definitions of
|
|
281
|
+
Python's tzdata package.
|
|
282
|
+
|
|
283
|
+
You can list all timezones available with this command:
|
|
284
|
+
|
|
285
|
+
\b
|
|
286
|
+
ics-query --available-timezones
|
|
287
|
+
|
|
288
|
+
By default the local time of the components is assumed.
|
|
289
|
+
In this example, two events happen at 6am, one in Berlin and one in Los Angeles.
|
|
290
|
+
Both are hours apart though.
|
|
291
|
+
|
|
292
|
+
\b
|
|
293
|
+
$ ics-query at 2024-08-20 Berlin-Los-Angeles.ics - | grep -E 'DTSTART|SUMMARY'
|
|
294
|
+
SUMMARY:6:00-7:00 Europe/Berlin 20th August
|
|
295
|
+
DTSTART;TZID=Europe/Berlin:20240820T060000
|
|
296
|
+
SUMMARY:6:00-7:00 Amerika/Los Angeles 20th August
|
|
297
|
+
DTSTART;TZID=America/Los_Angeles:20240820T060000
|
|
298
|
+
|
|
299
|
+
If you however wish to get all events in a certain timezone, use the --tz parameter.
|
|
300
|
+
In this example, the event that happens at the 19th of August at 21:00 in
|
|
301
|
+
Los Angeles is actually happening on the 20th in local Berlin time.
|
|
302
|
+
|
|
303
|
+
\b
|
|
304
|
+
$ ics-query at --tz=Europe/Berlin 2024-08-20 Berlin-Los-Angeles.ics - \\
|
|
305
|
+
| grep -E 'DTSTART|SUMMARY'
|
|
306
|
+
SUMMARY:6:00-7:00 Europe/Berlin 20th August
|
|
307
|
+
DTSTART;TZID=Europe/Berlin:20240820T060000
|
|
308
|
+
SUMMARY:6:00-7:00 Amerika/Los Angeles 20th August
|
|
309
|
+
DTSTART;TZID=Europe/Berlin:20240820T150000
|
|
310
|
+
SUMMARY:21:00-22:00 Amerika/Los Angeles 19th August
|
|
311
|
+
DTSTART;TZID=Europe/Berlin:20240820T060000
|
|
312
|
+
|
|
313
|
+
If you wish to get events in your local time, use --tz localtime.
|
|
314
|
+
If you like UTC, use --tz UTC.
|
|
315
|
+
|
|
316
|
+
You can also set the environment variable ICS_QUERY_TZ to the timezone instead of
|
|
317
|
+
passing --tz.
|
|
318
|
+
|
|
319
|
+
\b
|
|
320
|
+
Components
|
|
321
|
+
----------
|
|
322
|
+
|
|
323
|
+
We support different types of recurring components: VEVENT, VTODO, VJOURNAL.
|
|
324
|
+
You can specify which can be in the result using the --component parameter.
|
|
325
|
+
|
|
326
|
+
You can also set the environment variable ICS_QUERY_COMPONENT to the timezone
|
|
327
|
+
instead of passing --component.
|
|
328
|
+
|
|
126
329
|
""" # noqa: D301
|
|
127
330
|
|
|
128
331
|
|
|
129
332
|
pass_datetime = click.make_pass_decorator(parse.to_time)
|
|
130
333
|
|
|
131
334
|
|
|
132
|
-
@
|
|
335
|
+
@cli.command()
|
|
133
336
|
@click.argument("date", type=parse.to_time)
|
|
134
337
|
@arg_calendar
|
|
135
338
|
@arg_output
|
|
136
|
-
def at(calendar:
|
|
137
|
-
"""
|
|
339
|
+
def at(calendar: JoinedCalendars, output: ComponentsResult, date: Date):
|
|
340
|
+
"""Print occurrences at a certain date or time.
|
|
138
341
|
|
|
139
342
|
YEAR
|
|
140
343
|
|
|
@@ -156,7 +359,7 @@ def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
|
|
|
156
359
|
|
|
157
360
|
\b
|
|
158
361
|
Formats:
|
|
159
|
-
|
|
362
|
+
\b
|
|
160
363
|
YYYY-MM
|
|
161
364
|
YYYY-M
|
|
162
365
|
YYYYMM
|
|
@@ -175,7 +378,7 @@ def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
|
|
|
175
378
|
|
|
176
379
|
\b
|
|
177
380
|
Formats:
|
|
178
|
-
|
|
381
|
+
\b
|
|
179
382
|
YYYY-MM-DD
|
|
180
383
|
YYYY-M-D
|
|
181
384
|
YYYYMMDD
|
|
@@ -253,8 +456,237 @@ def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
|
|
|
253
456
|
ics-query at 19900101235959 # 1st January 1990, 23:59:59
|
|
254
457
|
ics-query at `date +%Y%m%d%H%M%S` # now
|
|
255
458
|
""" # noqa: D301
|
|
256
|
-
|
|
257
|
-
|
|
459
|
+
output.add_components(calendar.at(date))
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@cli.command()
|
|
463
|
+
@arg_calendar
|
|
464
|
+
@arg_output
|
|
465
|
+
def first(calendar: JoinedCalendars, output: ComponentsResult):
|
|
466
|
+
"""Print only the first occurrence.
|
|
467
|
+
|
|
468
|
+
This prints the first occurrence in each calendar that is given.
|
|
469
|
+
|
|
470
|
+
\b
|
|
471
|
+
This example prints the first event in calendar.ics:
|
|
472
|
+
\b
|
|
473
|
+
ics-query first --component VEVENT calendar.ics -
|
|
474
|
+
|
|
475
|
+
""" # noqa: D301
|
|
476
|
+
output.add_components(calendar.first())
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@cli.command()
|
|
480
|
+
@arg_calendar
|
|
481
|
+
@arg_output
|
|
482
|
+
def all(calendar: JoinedCalendars, output: ComponentsResult): # noqa: A001
|
|
483
|
+
"""Print all occurrences in a calendar.
|
|
484
|
+
|
|
485
|
+
The result is ordered by the start of the occurrences.
|
|
486
|
+
If you have multiple calendars, the result will contain
|
|
487
|
+
the occurrences of the first calendar before those of the second calendar
|
|
488
|
+
and so on.
|
|
489
|
+
|
|
490
|
+
\b
|
|
491
|
+
This example prints all events in calendar.ics:
|
|
492
|
+
\b
|
|
493
|
+
ics-query all --component VEVENT calendar.ics -
|
|
494
|
+
|
|
495
|
+
Note that calendars can create hundreds of occurrences and especially
|
|
496
|
+
contain endless repetitions. Use this with care as the output is
|
|
497
|
+
potentially enourmous. You can mitigate this by closing the OUTPUT
|
|
498
|
+
when you have enough e.g. with a head command.
|
|
499
|
+
""" # noqa: D301
|
|
500
|
+
output.add_components(calendar.all())
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@cli.command()
|
|
504
|
+
@click.argument("start", type=parse.to_time)
|
|
505
|
+
@click.argument("end", type=parse.to_time_and_delta)
|
|
506
|
+
@arg_calendar
|
|
507
|
+
@arg_output
|
|
508
|
+
def between(
|
|
509
|
+
start: parse.Date,
|
|
510
|
+
end: parse.DateAndDelta,
|
|
511
|
+
calendar: JoinedCalendars,
|
|
512
|
+
output: ComponentsResult,
|
|
513
|
+
):
|
|
514
|
+
"""Print occurrences between a START and an END.
|
|
515
|
+
|
|
516
|
+
The start is inclusive, the end is exclusive.
|
|
517
|
+
|
|
518
|
+
This example returns the events within the next week:
|
|
519
|
+
|
|
520
|
+
\b
|
|
521
|
+
ics-query between --component VEVENT `date +%Y%m%d` +7d calendar.ics -
|
|
522
|
+
|
|
523
|
+
This example saves the events from the 1st of May 2024 to the 10th of June in
|
|
524
|
+
events.ics:
|
|
525
|
+
|
|
526
|
+
\b
|
|
527
|
+
ics-query between --component VEVENT 2024-5-1 2024-6-10 calendar.ics events.ics
|
|
528
|
+
|
|
529
|
+
In this example, you can check what is happening on New Years Eve 2025 around
|
|
530
|
+
midnight:
|
|
531
|
+
|
|
532
|
+
\b
|
|
533
|
+
ics-query between 2025-12-31T21:00 +6h calendar.ics events.ics
|
|
534
|
+
|
|
535
|
+
\b
|
|
536
|
+
Absolute Time
|
|
537
|
+
-------------
|
|
538
|
+
|
|
539
|
+
START must be specified as an absolute time.
|
|
540
|
+
END can be absolute or relative to START, see Relative Time below.
|
|
541
|
+
|
|
542
|
+
Each of the formats specify the earliest time e.g. the start of a day.
|
|
543
|
+
Thus, if START == END, there are 0 seconds in between and the result is
|
|
544
|
+
only what happens during that time or starts exactly at that time.
|
|
545
|
+
|
|
546
|
+
YEAR
|
|
547
|
+
|
|
548
|
+
Specifiy the start of the year.
|
|
549
|
+
|
|
550
|
+
\b
|
|
551
|
+
Formats:
|
|
552
|
+
\b
|
|
553
|
+
YYYY
|
|
554
|
+
\b
|
|
555
|
+
Examples:
|
|
556
|
+
\b
|
|
557
|
+
2024 # start of 2024
|
|
558
|
+
`date +%Y` # this year
|
|
559
|
+
|
|
560
|
+
MONTH
|
|
561
|
+
|
|
562
|
+
The start of the month.
|
|
563
|
+
|
|
564
|
+
\b
|
|
565
|
+
Formats:
|
|
566
|
+
\b
|
|
567
|
+
YYYY-MM
|
|
568
|
+
YYYY-M
|
|
569
|
+
YYYYMM
|
|
570
|
+
\b
|
|
571
|
+
Examples:
|
|
572
|
+
\b
|
|
573
|
+
2019-10 # October 2019
|
|
574
|
+
1990-01 # January 1990
|
|
575
|
+
1990-1 # January 1990
|
|
576
|
+
199001 # January 1990
|
|
577
|
+
`date +%Y%m` # this month
|
|
578
|
+
|
|
579
|
+
DAY
|
|
580
|
+
|
|
581
|
+
The start of the day
|
|
582
|
+
|
|
583
|
+
\b
|
|
584
|
+
Formats:
|
|
585
|
+
\b
|
|
586
|
+
YYYY-MM-DD
|
|
587
|
+
YYYY-M-D
|
|
588
|
+
YYYYMMDD
|
|
589
|
+
\b
|
|
590
|
+
Examples:
|
|
591
|
+
\b
|
|
592
|
+
1990-01-01 # 1st January 1990
|
|
593
|
+
1990-1-1 # 1st January 1990
|
|
594
|
+
19900101 # 1st January 1990
|
|
595
|
+
`date +%Y%m%d` # today
|
|
596
|
+
|
|
597
|
+
HOUR
|
|
598
|
+
|
|
599
|
+
The start of the hour.
|
|
600
|
+
|
|
601
|
+
\b
|
|
602
|
+
Formats:
|
|
603
|
+
\b
|
|
604
|
+
YYYY-MM-DD HH
|
|
605
|
+
YYYY-MM-DDTHH
|
|
606
|
+
YYYY-M-DTH
|
|
607
|
+
YYYYMMDDTHH
|
|
608
|
+
YYYYMMDDHH
|
|
609
|
+
\b
|
|
610
|
+
Examples:
|
|
611
|
+
\b
|
|
612
|
+
1990-01-01 01 # 1st January 1990, 1am
|
|
613
|
+
1990-01-01T01 # 1st January 1990, 1am
|
|
614
|
+
1990-1-1T17 # 1st January 1990, 17:00
|
|
615
|
+
19900101T23 # 1st January 1990, 23:00
|
|
616
|
+
1990010123 # 1st January 1990, 23:00
|
|
617
|
+
`date +%Y%m%d%H` # this hour
|
|
618
|
+
|
|
619
|
+
MINUTE
|
|
620
|
+
|
|
621
|
+
The start of a minute.
|
|
622
|
+
|
|
623
|
+
\b
|
|
624
|
+
Formats:
|
|
625
|
+
\b
|
|
626
|
+
YYYY-MM-DD HH:MM
|
|
627
|
+
YYYY-MM-DDTHH:MM
|
|
628
|
+
YYYY-M-DTH:M
|
|
629
|
+
YYYYMMDDTHHMM
|
|
630
|
+
YYYYMMDDHHMM
|
|
631
|
+
\b
|
|
632
|
+
Examples:
|
|
633
|
+
\b
|
|
634
|
+
1990-01-01 10:10 # 1st January 1990, 10:10am
|
|
635
|
+
1990-01-01T10:10 # 1st January 1990, 10:10am
|
|
636
|
+
1990-1-1T7:2 # 1st January 1990, 07:02
|
|
637
|
+
19900101T2359 # 1st January 1990, 23:59
|
|
638
|
+
199001012359 # 1st January 1990, 23:59
|
|
639
|
+
`date +%Y%m%d%H%M` # this minute
|
|
640
|
+
|
|
641
|
+
SECOND
|
|
642
|
+
|
|
643
|
+
A precise time. RFC 5545 calendars are specified to the second.
|
|
644
|
+
This is the most precise format to specify times.
|
|
645
|
+
|
|
646
|
+
\b
|
|
647
|
+
Formats:
|
|
648
|
+
\b
|
|
649
|
+
YYYY-MM-DD HH:MM:SS
|
|
650
|
+
YYYY-MM-DDTHH:MM:SS
|
|
651
|
+
YYYY-M-DTH:M:S
|
|
652
|
+
YYYYMMDDTHHMMSS
|
|
653
|
+
YYYYMMDDHHMMSS
|
|
654
|
+
\b
|
|
655
|
+
Examples:
|
|
656
|
+
\b
|
|
657
|
+
1990-01-01 10:10:00 # 1st January 1990, 10:10am
|
|
658
|
+
1990-01-01T10:10:00 # 1st January 1990, 10:10am
|
|
659
|
+
1990-1-1T7:2:30 # 1st January 1990, 07:02:30
|
|
660
|
+
19901231T235959 # 31st December 1990, 23:59:59
|
|
661
|
+
19900101235959 # 1st January 1990, 23:59:59
|
|
662
|
+
`date +%Y%m%d%H%M%S` # now
|
|
663
|
+
\b
|
|
664
|
+
Relative Time
|
|
665
|
+
-------------
|
|
666
|
+
|
|
667
|
+
The END argument can be a time range.
|
|
668
|
+
The + at the beginning is optional but makes for a better reading.
|
|
669
|
+
|
|
670
|
+
\b
|
|
671
|
+
Examples:
|
|
672
|
+
\b
|
|
673
|
+
Add 10 days to START: +10d
|
|
674
|
+
Add 24 hours to START: +1d or +24h
|
|
675
|
+
Add 3 hours to START: +3h
|
|
676
|
+
Add 30 minutes to START: +30m
|
|
677
|
+
Add 1000 seconds to START: +1000s
|
|
678
|
+
\b
|
|
679
|
+
You can also combine the ranges:
|
|
680
|
+
Add 1 day and 12 hours to START: +1d12h
|
|
681
|
+
Add 3 hours and 15 minutes to START: +3h15m
|
|
682
|
+
|
|
683
|
+
""" # noqa: D301
|
|
684
|
+
output.add_components(calendar.between(start, end))
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def main():
|
|
688
|
+
"""Run the program."""
|
|
689
|
+
cli(auto_envvar_prefix=ENV_PREFIX)
|
|
258
690
|
|
|
259
691
|
|
|
260
|
-
__all__ = ["main"]
|
|
692
|
+
__all__ = ["main", "ENV_PREFIX", "cli"]
|
ics_query/parse.py
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
+
# ics-query
|
|
2
|
+
# Copyright (C) 2024 Nicco Kunzmann
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
1
16
|
"""Functions for parsing the content."""
|
|
2
17
|
|
|
3
18
|
from __future__ import annotations
|
|
4
19
|
|
|
20
|
+
import datetime
|
|
5
21
|
import re
|
|
22
|
+
from typing import Union
|
|
6
23
|
|
|
7
|
-
|
|
24
|
+
Date = tuple[int]
|
|
25
|
+
DateAndDelta = Union[Date, datetime.timedelta]
|
|
8
26
|
|
|
9
27
|
|
|
10
28
|
class InvalidTimeFormat(ValueError):
|
|
@@ -21,14 +39,22 @@ REGEX_TIME = re.compile(
|
|
|
21
39
|
r"$"
|
|
22
40
|
)
|
|
23
41
|
|
|
42
|
+
REGEX_TIMEDELTA = re.compile(
|
|
43
|
+
r"^\+?(?:(?P<days>\d+)d)?"
|
|
44
|
+
r"(?:(?P<hours>\d+)h)?"
|
|
45
|
+
r"(?:(?P<minutes>\d+)m)?"
|
|
46
|
+
r"(?:(?P<seconds>\d+)s)?"
|
|
47
|
+
r"$"
|
|
48
|
+
)
|
|
49
|
+
|
|
24
50
|
|
|
25
|
-
def to_time(dt: str) ->
|
|
51
|
+
def to_time(dt: str) -> Date:
|
|
26
52
|
"""Parse the time and date."""
|
|
27
53
|
parsed_dt = REGEX_TIME.match(dt)
|
|
28
54
|
if parsed_dt is None:
|
|
29
55
|
raise InvalidTimeFormat(dt)
|
|
30
56
|
|
|
31
|
-
def group(group_name: str) ->
|
|
57
|
+
def group(group_name: str) -> Date:
|
|
32
58
|
"""Return a group's value."""
|
|
33
59
|
result = parsed_dt.group(group_name)
|
|
34
60
|
while result and result[0] not in "0123456789":
|
|
@@ -47,4 +73,15 @@ def to_time(dt: str) -> DateArgument:
|
|
|
47
73
|
)
|
|
48
74
|
|
|
49
75
|
|
|
50
|
-
|
|
76
|
+
def to_time_and_delta(dt: str) -> DateAndDelta:
|
|
77
|
+
"""Parse to a absolute time or timedelta."""
|
|
78
|
+
parsed_td = REGEX_TIMEDELTA.match(dt)
|
|
79
|
+
if parsed_td is None:
|
|
80
|
+
return to_time(dt)
|
|
81
|
+
kw = {k: int(v) for k, v in parsed_td.groupdict().items() if v is not None}
|
|
82
|
+
if not kw:
|
|
83
|
+
raise InvalidTimeFormat(dt)
|
|
84
|
+
return datetime.timedelta(**kw)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = ["to_time", "Date", "to_time_and_delta", "DateAndDelta"]
|