ics-query 0.1.dev8__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.
Files changed (41) hide show
  1. ics_query/__init__.py +18 -2
  2. ics_query/__main__.py +15 -0
  3. ics_query/_version.py +2 -2
  4. ics_query/cli.py +618 -18
  5. ics_query/parse.py +78 -4
  6. ics_query/query.py +71 -0
  7. ics_query/tests/conftest.py +39 -12
  8. ics_query/tests/runs/all --tz Singapore one-event.ics -.run +9 -0
  9. ics_query/tests/runs/all three-events.ics -.run +33 -0
  10. ics_query/tests/runs/at 2019-03-04 multiple-calendars.ics -.run +20 -0
  11. ics_query/tests/runs/at 2019-03-04 one-event-twice.ics -.run +18 -0
  12. ics_query/tests/runs/at 2019-03-07 multiple-calendars.ics -.run +11 -0
  13. ics_query/tests/runs/at 2024-08-20 Berlin-Los-Angeles.ics -.run +23 -0
  14. ics_query/tests/runs/between 20240823 4d recurring-work-events.ics -.run +24 -0
  15. ics_query/tests/runs/calendars/Berlin-Los-Angeles.ics +381 -0
  16. ics_query/tests/runs/calendars/empty-calendar.ics +7 -0
  17. ics_query/tests/runs/calendars/empty-file.ics +0 -0
  18. ics_query/tests/runs/calendars/multiple-calendars.ics +71 -0
  19. ics_query/tests/runs/calendars/one-event-twice.ics +68 -0
  20. ics_query/tests/runs/calendars/recurring-work-events.ics +223 -0
  21. ics_query/tests/runs/calendars/simple-journal.ics +15 -0
  22. ics_query/tests/runs/calendars/simple-todo.ics +15 -0
  23. ics_query/tests/runs/calendars/three-events.ics +37 -0
  24. ics_query/tests/runs/first -c VJOURNAL -c VEVENT one-event.ics -.run +9 -0
  25. ics_query/tests/runs/first -c VJOURNAL one-event.ics -.run +0 -0
  26. ics_query/tests/runs/first -c VJOURNAL simple-journal.ics -.run +12 -0
  27. ics_query/tests/runs/first -c VTODO -c VJOURNAL simple-todo.ics -.run +10 -0
  28. ics_query/tests/runs/first -c VTODO simple-todo.ics -.run +10 -0
  29. ics_query/tests/runs/first empty-calendar.ics -.run +0 -0
  30. ics_query/tests/runs/first empty-file.ics -.run +0 -0
  31. ics_query/tests/runs/first recurring-work-events.ics -.run +12 -0
  32. ics_query/tests/test_command_line.py +43 -0
  33. ics_query/tests/test_parse_date.py +81 -0
  34. ics_query/tests/test_parse_timedelta.py +40 -0
  35. ics_query/version.py +33 -3
  36. {ics_query-0.1.dev8.dist-info → ics_query-0.3.1b0.dist-info}/METADATA +393 -37
  37. ics_query-0.3.1b0.dist-info/RECORD +43 -0
  38. ics_query-0.1.dev8.dist-info/RECORD +0 -16
  39. {ics_query-0.1.dev8.dist-info → ics_query-0.3.1b0.dist-info}/WHEEL +0 -0
  40. {ics_query-0.1.dev8.dist-info → ics_query-0.3.1b0.dist-info}/entry_points.txt +0 -0
  41. {ics_query-0.1.dev8.dist-info → ics_query-0.3.1b0.dist-info}/licenses/LICENSE +0 -0
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,23 +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
- from icalendar import Calendar
13
- from recurring_ical_events import CalendarQuery
27
+ from icalendar.cal import Calendar, Component
14
28
 
15
29
  from . import parse
30
+ from .query import Query
31
+ from .version import cli_version
16
32
 
17
33
  if t.TYPE_CHECKING:
18
34
  from io import FileIO
19
35
 
36
+ import recurring_ical_events
20
37
  from icalendar.cal import Component
21
38
 
22
- from .parse import DateArgument
39
+ from .parse import Date
23
40
 
24
41
  print = functools.partial(print, file=sys.stderr) # noqa: A001
25
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
+
26
62
 
27
63
  class ComponentsResult:
28
64
  """Output interface for components."""
@@ -35,6 +71,11 @@ class ComponentsResult:
35
71
  """Return a component."""
36
72
  self._file.write(component.to_ical())
37
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
+
38
79
 
39
80
  class ComponentsResultArgument(click.File):
40
81
  """Argument for the result."""
@@ -50,6 +91,39 @@ class ComponentsResultArgument(click.File):
50
91
  return ComponentsResult(file)
51
92
 
52
93
 
94
+ class JoinedCalendars:
95
+ def __init__(
96
+ self, calendars: list[Calendar], timezone: str, components: t.Sequence[str]
97
+ ):
98
+ """Join multiple calendars."""
99
+ self.queries = [
100
+ Query(calendar, timezone, components=components) for calendar in calendars
101
+ ]
102
+
103
+ def at(self, dt: tuple[int]) -> t.Generator[Component]:
104
+ """Return the components."""
105
+ for query in self.queries:
106
+ yield from query.at(dt)
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
+
126
+
53
127
  class CalendarQueryInputArgument(click.File):
54
128
  """Argument for the result."""
55
129
 
@@ -61,32 +135,558 @@ class CalendarQueryInputArgument(click.File):
61
135
  ) -> recurring_ical_events.CalendarQuery:
62
136
  """Return a CalendarQuery."""
63
137
  file = super().convert(value, param, ctx)
64
- calendar = Calendar.from_ical(file.read())
65
- return recurring_ical_events.of(calendar)
138
+ calendars = Calendar.from_ical(file.read(), multiple=True)
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"))
66
166
 
167
+ @functools.wraps(func)
168
+ def wrapper(*args, component=(), tz="", **kw): # noqa: ARG001
169
+ """Remove some parameters."""
170
+ return func(*args, **kw)
67
171
 
68
- arg_calendar = click.argument("calendar", type=CalendarQueryInputArgument("rb"))
69
- arg_output = click.argument("output", type=ComponentsResultArgument("wb"))
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
+ """
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
189
+
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)
70
231
 
71
232
 
72
233
  @click.group()
73
- def main():
74
- """Simple program that greets NAME for a total of COUNT times."""
75
- # sys.stdout = sys.stderr # remove accidential print impact
234
+ @click.version_option(cli_version)
235
+ @opt_available_timezones()
236
+ @opt_license()
237
+ def cli():
238
+ """Find out what happens in ICS calendar files.
239
+
240
+ ics-query can query and filter RFC 5545 compatible .ics files.
241
+ Components are events, journal entries and TODOs.
242
+
243
+ \b
244
+ Common Parameters
245
+ -----------------
246
+
247
+ Common parameters are described below.
248
+
249
+ CALENDAR
250
+
251
+ The CALENDAR is a readable file with one or more ICS calendars in it.
252
+ If CALENDAR is "-", then the standard input is used.
253
+
254
+ OUTPUT
255
+
256
+ This is the OUTPUT file for the result.
257
+ It is usually a path to a file that can be written to.
258
+ If OUTPUT is "-", then the standard output is used.
259
+
260
+ \b
261
+ Calculation
262
+ -----------
263
+
264
+ An event can be very long. If you request smaller time spans or a time as
265
+ exact as a second, the event will still occur within this time span if it
266
+ happens during that time.
267
+
268
+ Generally, an event occurs within a time span if this applies:
269
+
270
+ event.DTSTART <= span.DTEND and span.DTSTART < event.DTEND
271
+
272
+ The START is INCLUSIVE, then END is EXCLUSIVE.
273
+
274
+ \b
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
+
329
+ """ # noqa: D301
76
330
 
77
331
 
78
332
  pass_datetime = click.make_pass_decorator(parse.to_time)
79
333
 
80
334
 
81
- @main.command()
335
+ @cli.command()
82
336
  @click.argument("date", type=parse.to_time)
83
337
  @arg_calendar
84
338
  @arg_output
85
- def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
86
- """Get the components at a certain time."""
87
- for event in calendar.at(date):
88
- print("debug")
89
- output.add_component(event)
339
+ def at(calendar: JoinedCalendars, output: ComponentsResult, date: Date):
340
+ """Print occurrences at a certain date or time.
341
+
342
+ YEAR
343
+
344
+ All occurrences in this year.
345
+
346
+ \b
347
+ Formats:
348
+ \b
349
+ YYYY
350
+ \b
351
+ Examples:
352
+ \b
353
+ ics-query at 2024 # all occurrences in year 2024
354
+ ics-query at `date +%Y` # all occurrences in this year
355
+
356
+ MONTH
357
+
358
+ All occurrences in this month.
359
+
360
+ \b
361
+ Formats:
362
+ \b
363
+ YYYY-MM
364
+ YYYY-M
365
+ YYYYMM
366
+ \b
367
+ Examples:
368
+ \b
369
+ ics-query at 2019-10 # October 2019
370
+ ics-query at 1990-01 # January 1990
371
+ ics-query at 1990-1 # January 1990
372
+ ics-query at 199001 # January 1990
373
+ ics-query at `date +%Y%m` # this month
374
+
375
+ DAY
376
+
377
+ All occurrences in one day.
378
+
379
+ \b
380
+ Formats:
381
+ \b
382
+ YYYY-MM-DD
383
+ YYYY-M-D
384
+ YYYYMMDD
385
+ \b
386
+ Examples:
387
+ \b
388
+ ics-query at 1990-01-01 # 1st January 1990
389
+ ics-query at 1990-1-1 # 1st January 1990
390
+ ics-query at 19900101 # 1st January 1990
391
+ ics-query at `date +%Y%m%d` # today
392
+
393
+ HOUR
394
+
395
+ All occurrences within one hour.
396
+
397
+ \b
398
+ Formats:
399
+ \b
400
+ YYYY-MM-DD HH
401
+ YYYY-MM-DDTHH
402
+ YYYY-M-DTH
403
+ YYYYMMDDTHH
404
+ YYYYMMDDHH
405
+ \b
406
+ Examples:
407
+ \b
408
+ ics-query at 1990-01-01 00 # 1st January 1990, 12am - 1am
409
+ ics-query at 1990-01-01T00 # 1st January 1990, 12am - 1am
410
+ ics-query at 1990-1-1T17 # 1st January 1990, 17:00 - 18:00
411
+ ics-query at 19900101T23 # 1st January 1990, 23:00 - midnight
412
+ ics-query at 1990010123 # 1st January 1990, 23:00 - midnight
413
+ ics-query at `date +%Y%m%d%H` # this hour
414
+
415
+ MINUTE
416
+
417
+ All occurrences within one minute.
418
+
419
+ \b
420
+ Formats:
421
+ \b
422
+ YYYY-MM-DD HH:MM
423
+ YYYY-MM-DDTHH:MM
424
+ YYYY-M-DTH:M
425
+ YYYYMMDDTHHMM
426
+ YYYYMMDDHHMM
427
+ \b
428
+ Examples:
429
+ \b
430
+ ics-query at 1990-01-01 10:10 # 1st January 1990, 10:10am - 10:11am
431
+ ics-query at 1990-01-01T10:10 # 1st January 1990, 10:10am - 10:11am
432
+ ics-query at 1990-1-1T7:2 # 1st January 1990, 07:02 - 07:03
433
+ ics-query at 19900101T2359 # 1st January 1990, 23:59 - midnight
434
+ ics-query at 199001012359 # 1st January 1990, 23:59 - midnight
435
+ ics-query at `date +%Y%m%d%H%M` # this minute
436
+
437
+ SECOND
438
+
439
+ All occurrences at a precise time.
440
+
441
+ \b
442
+ Formats:
443
+ \b
444
+ YYYY-MM-DD HH:MM:SS
445
+ YYYY-MM-DDTHH:MM:SS
446
+ YYYY-M-DTH:M:S
447
+ YYYYMMDDTHHMMSS
448
+ YYYYMMDDHHMMSS
449
+ \b
450
+ Examples:
451
+ \b
452
+ ics-query at 1990-01-01 10:10:00 # 1st January 1990, 10:10am
453
+ ics-query at 1990-01-01T10:10:00 # 1st January 1990, 10:10am
454
+ ics-query at 1990-1-1T7:2:30 # 1st January 1990, 07:02:30
455
+ ics-query at 19901231T235959 # 31st December 1990, 23:59:59
456
+ ics-query at 19900101235959 # 1st January 1990, 23:59:59
457
+ ics-query at `date +%Y%m%d%H%M%S` # now
458
+ """ # noqa: D301
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)
90
690
 
91
691
 
92
- __all__ = ["main"]
692
+ __all__ = ["main", "ENV_PREFIX", "cli"]