ics-query 0.1.1a0__tar.gz → 0.2.0a0__tar.gz

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 (36) hide show
  1. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/PKG-INFO +54 -12
  2. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/README.md +50 -8
  3. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/_version.py +2 -2
  4. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/cli.py +222 -7
  5. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/parse.py +26 -4
  6. ics_query-0.2.0a0/ics_query/tests/runs/between 20240823 4d recurring-work-events.ics -.run +24 -0
  7. ics_query-0.2.0a0/ics_query/tests/runs/calendars/empty-calendar.ics +7 -0
  8. ics_query-0.2.0a0/ics_query/tests/runs/calendars/empty-file.ics +0 -0
  9. ics_query-0.2.0a0/ics_query/tests/runs/calendars/recurring-work-events.ics +223 -0
  10. ics_query-0.2.0a0/ics_query/tests/runs/first empty-calendar.ics -.run +0 -0
  11. ics_query-0.2.0a0/ics_query/tests/runs/first empty-file.ics -.run +0 -0
  12. ics_query-0.2.0a0/ics_query/tests/runs/first recurring-work-events.ics -.run +12 -0
  13. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/test_parse_date.py +8 -5
  14. ics_query-0.2.0a0/ics_query/tests/test_parse_timedelta.py +25 -0
  15. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/pyproject.toml +2 -2
  16. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/tox.ini +3 -1
  17. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/.github/FUNDING.yml +0 -0
  18. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/.github/dependabot.yml +0 -0
  20. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/.github/workflows/tests.yml +0 -0
  21. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/.gitignore +0 -0
  22. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/LICENSE +0 -0
  23. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics-query +0 -0
  24. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/__init__.py +0 -0
  25. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/__main__.py +0 -0
  26. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/__init__.py +0 -0
  27. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/conftest.py +0 -0
  28. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/runs/at 2019-03-04 multiple-calendars.ics -.run +0 -0
  29. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/runs/at 2019-03-04 one-event-twice.ics -.run +0 -0
  30. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/runs/at 2019-03-04 one-event.ics -.run +0 -0
  31. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/runs/at 2019-03-07 multiple-calendars.ics -.run +0 -0
  32. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/runs/calendars/multiple-calendars.ics +0 -0
  33. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/runs/calendars/one-event-twice.ics +0 -0
  34. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/runs/calendars/one-event.ics +0 -0
  35. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/tests/test_command_line.py +0 -0
  36. {ics_query-0.1.1a0 → ics_query-0.2.0a0}/ics_query/version.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ics-query
3
- Version: 0.1.1a0
3
+ Version: 0.2.0a0
4
4
  Summary: Find out what happens in ICS calendar files - query and filter RFC 5545 compatible .ics files for events, journals, TODOs and more.
5
5
  Project-URL: Homepage, https://github.com/niccokunzmann/ics-query/
6
6
  Project-URL: Repository, https://github.com/niccokunzmann/ics-query/
7
- Project-URL: source_archive, https://github.com/niccokunzmann/ics-query/archive/fb12ef2822d23f693aa5f9faaa0cb97fa9f67516.zip
7
+ Project-URL: source_archive, https://github.com/niccokunzmann/ics-query/archive/c7eeb503fae7da069308a27a37232be96851c3e5.zip
8
8
  Project-URL: Issues, https://github.com/niccokunzmann/ics-query/issues
9
9
  Project-URL: Documentation, https://github.com/niccokunzmann/ics-query/
10
10
  Project-URL: Changelog, https://github.com/niccokunzmann/ics-query/#changelog
@@ -695,10 +695,10 @@ Classifier: Programming Language :: Python :: 3.10
695
695
  Classifier: Programming Language :: Python :: 3.11
696
696
  Classifier: Programming Language :: Python :: 3.12
697
697
  Classifier: Topic :: Office/Business :: Scheduling
698
- Requires-Python: >=3.8
698
+ Requires-Python: >=3.9
699
699
  Requires-Dist: click
700
700
  Requires-Dist: icalendar
701
- Requires-Dist: recurring-ical-events
701
+ Requires-Dist: recurring-ical-events<4,>=3.2.0
702
702
  Provides-Extra: test
703
703
  Requires-Dist: pytest; extra == 'test'
704
704
  Description-Content-Type: text/markdown
@@ -780,8 +780,46 @@ ics-query at <date-time> calendar.ics -
780
780
  Please see the command documentation for more help:
781
781
 
782
782
  ```shell
783
- ics-query --help
784
783
  ics-query at --help
784
+ ics-query --help
785
+ ```
786
+
787
+ ### Events within a Time Span
788
+
789
+ You can query which events happen between certain times:
790
+
791
+ ```shell
792
+ ics-query between <start> <end> calendar.ics -
793
+ ics-query between <start> <duration> calendar.ics -
794
+ ```
795
+
796
+ Please see the command documentation for more help:
797
+
798
+ ```shell
799
+ ics-query between --help
800
+ ics-query --help
801
+ ```
802
+
803
+ ### Time Span Examples
804
+
805
+ This example returns the events within the next week:
806
+
807
+ ```shell
808
+ ics-query between `date +%Y%m%d` +7d calendar.ics -
809
+ ```
810
+
811
+ This example saves the events from the 1st of May 2024 to the 10th of June in
812
+ events.ics:
813
+
814
+ ```shell
815
+ ics-query between 2024-5-1 2024-6-10 calendar.ics events.ics
816
+ ```
817
+
818
+ In this example, you can check what is happening on New Years Eve 2025 around
819
+ midnight:
820
+
821
+ ```shell
822
+ ics-query between 2025-12-31T21:00 +6h calendar.ics events.ics
785
823
  ```
786
824
 
787
825
  ## Vision
@@ -810,13 +848,10 @@ ics-query --components VTODO at 2029-12-24 calendar.ics
810
848
 
811
849
  ### `ics-query at` - time ranges
812
850
 
813
-
814
851
  ### `ics-query --output=count` - count occurrences
815
852
 
816
-
817
853
  ### `ics-query --output=ics` - use ics as output (default)
818
854
 
819
-
820
855
  ### `ics-query --select-index` - reduce output size
821
856
 
822
857
  Examples: `0,2,4` `0-10`
@@ -832,10 +867,8 @@ ics-query between dt duration
832
867
 
833
868
  ### `ics-query --select-component` - filter for components
834
869
 
835
-
836
870
  ### `ics-query --select-uid` - filter by uid
837
871
 
838
-
839
872
  ## How to edit an event
840
873
 
841
874
  To edit a component like an event, you can append it to the calendar and increase the sequence number.
@@ -920,7 +953,7 @@ Run this to format the code and show problems:
920
953
  tox -e ruff
921
954
  ```
922
955
 
923
- ## New Release
956
+ ### New Release
924
957
 
925
958
  To release new versions,
926
959
 
@@ -938,10 +971,15 @@ To release new versions,
938
971
 
939
972
  ## Changelog
940
973
 
974
+ - v0.2.0a
975
+
976
+ - Add `ics-query first <calendar> <output>` for earliest occurrences
977
+ - Add `ics-query between <span_start> <span_stop> <calendar> <output>` to query time ranges
978
+
941
979
  - v0.1.1a
942
980
 
943
981
  - Add `--version`
944
- - Add `ics-query at <date>`
982
+ - Add `ics-query at <date> <calendar> <output>`
945
983
  - Add support for multiple calendars in one input
946
984
 
947
985
  - v0.1.0a
@@ -952,3 +990,7 @@ To release new versions,
952
990
  - v0.0.1a
953
991
 
954
992
  - first version
993
+
994
+ ## Related Work
995
+
996
+ - [icalBuddy](https://hasseg.org/icalBuddy/)
@@ -75,8 +75,46 @@ ics-query at <date-time> calendar.ics -
75
75
  Please see the command documentation for more help:
76
76
 
77
77
  ```shell
78
- ics-query --help
79
78
  ics-query at --help
79
+ ics-query --help
80
+ ```
81
+
82
+ ### Events within a Time Span
83
+
84
+ You can query which events happen between certain times:
85
+
86
+ ```shell
87
+ ics-query between <start> <end> calendar.ics -
88
+ ics-query between <start> <duration> calendar.ics -
89
+ ```
90
+
91
+ Please see the command documentation for more help:
92
+
93
+ ```shell
94
+ ics-query between --help
95
+ ics-query --help
96
+ ```
97
+
98
+ ### Time Span Examples
99
+
100
+ This example returns the events within the next week:
101
+
102
+ ```shell
103
+ ics-query between `date +%Y%m%d` +7d calendar.ics -
104
+ ```
105
+
106
+ This example saves the events from the 1st of May 2024 to the 10th of June in
107
+ events.ics:
108
+
109
+ ```shell
110
+ ics-query between 2024-5-1 2024-6-10 calendar.ics events.ics
111
+ ```
112
+
113
+ In this example, you can check what is happening on New Years Eve 2025 around
114
+ midnight:
115
+
116
+ ```shell
117
+ ics-query between 2025-12-31T21:00 +6h calendar.ics events.ics
80
118
  ```
81
119
 
82
120
  ## Vision
@@ -105,13 +143,10 @@ ics-query --components VTODO at 2029-12-24 calendar.ics
105
143
 
106
144
  ### `ics-query at` - time ranges
107
145
 
108
-
109
146
  ### `ics-query --output=count` - count occurrences
110
147
 
111
-
112
148
  ### `ics-query --output=ics` - use ics as output (default)
113
149
 
114
-
115
150
  ### `ics-query --select-index` - reduce output size
116
151
 
117
152
  Examples: `0,2,4` `0-10`
@@ -127,10 +162,8 @@ ics-query between dt duration
127
162
 
128
163
  ### `ics-query --select-component` - filter for components
129
164
 
130
-
131
165
  ### `ics-query --select-uid` - filter by uid
132
166
 
133
-
134
167
  ## How to edit an event
135
168
 
136
169
  To edit a component like an event, you can append it to the calendar and increase the sequence number.
@@ -215,7 +248,7 @@ Run this to format the code and show problems:
215
248
  tox -e ruff
216
249
  ```
217
250
 
218
- ## New Release
251
+ ### New Release
219
252
 
220
253
  To release new versions,
221
254
 
@@ -233,10 +266,15 @@ To release new versions,
233
266
 
234
267
  ## Changelog
235
268
 
269
+ - v0.2.0a
270
+
271
+ - Add `ics-query first <calendar> <output>` for earliest occurrences
272
+ - Add `ics-query between <span_start> <span_stop> <calendar> <output>` to query time ranges
273
+
236
274
  - v0.1.1a
237
275
 
238
276
  - Add `--version`
239
- - Add `ics-query at <date>`
277
+ - Add `ics-query at <date> <calendar> <output>`
240
278
  - Add support for multiple calendars in one input
241
279
 
242
280
  - v0.1.0a
@@ -247,3 +285,7 @@ To release new versions,
247
285
  - v0.0.1a
248
286
 
249
287
  - first version
288
+
289
+ ## Related Work
290
+
291
+ - [icalBuddy](https://hasseg.org/icalBuddy/)
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.1.1a0'
16
- __version_tuple__ = version_tuple = (0, 1, 1)
15
+ __version__ = version = '0.2.0a0'
16
+ __version_tuple__ = version_tuple = (0, 2, 0)
@@ -10,7 +10,6 @@ import typing as t
10
10
  import click
11
11
  import recurring_ical_events
12
12
  from icalendar.cal import Calendar, Component
13
- from recurring_ical_events import CalendarQuery
14
13
 
15
14
  from . import parse
16
15
  from .version import __version__
@@ -20,7 +19,7 @@ if t.TYPE_CHECKING:
20
19
 
21
20
  from icalendar.cal import Component
22
21
 
23
- from .parse import DateArgument
22
+ from .parse import Date
24
23
 
25
24
  print = functools.partial(print, file=sys.stderr) # noqa: A001
26
25
 
@@ -36,6 +35,11 @@ class ComponentsResult:
36
35
  """Return a component."""
37
36
  self._file.write(component.to_ical())
38
37
 
38
+ def add_components(self, components: t.Iterable[Component]):
39
+ """Add all components."""
40
+ for component in components:
41
+ self.add_component(component)
42
+
39
43
 
40
44
  class ComponentsResultArgument(click.File):
41
45
  """Argument for the result."""
@@ -61,6 +65,19 @@ class JoinedCalendars:
61
65
  for query in self.queries:
62
66
  yield from query.at(dt)
63
67
 
68
+ def first(self) -> t.Generator[Component]:
69
+ """Return the first events of all calendars."""
70
+ for query in self.queries:
71
+ for component in query.all():
72
+ yield component
73
+ break
74
+
75
+ def between(
76
+ self, start: parse.Date, end: parse.DateAndDelta
77
+ ) -> t.Generator[Component]:
78
+ for query in self.queries:
79
+ yield from query.between(start, end)
80
+
64
81
 
65
82
  class CalendarQueryInputArgument(click.File):
66
83
  """Argument for the result."""
@@ -133,7 +150,7 @@ pass_datetime = click.make_pass_decorator(parse.to_time)
133
150
  @click.argument("date", type=parse.to_time)
134
151
  @arg_calendar
135
152
  @arg_output
136
- def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
153
+ def at(calendar: JoinedCalendars, output: ComponentsResult, date: Date):
137
154
  """Occurrences at a certain dates.
138
155
 
139
156
  YEAR
@@ -156,7 +173,7 @@ def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
156
173
 
157
174
  \b
158
175
  Formats:
159
-
176
+ \b
160
177
  YYYY-MM
161
178
  YYYY-M
162
179
  YYYYMM
@@ -175,7 +192,7 @@ def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
175
192
 
176
193
  \b
177
194
  Formats:
178
-
195
+ \b
179
196
  YYYY-MM-DD
180
197
  YYYY-M-D
181
198
  YYYYMMDD
@@ -253,8 +270,206 @@ def at(calendar: CalendarQuery, output: ComponentsResult, date: DateArgument):
253
270
  ics-query at 19900101235959 # 1st January 1990, 23:59:59
254
271
  ics-query at `date +%Y%m%d%H%M%S` # now
255
272
  """ # noqa: D301
256
- for event in calendar.at(date):
257
- output.add_component(event)
273
+ output.add_components(calendar.at(date))
274
+
275
+
276
+ @main.command()
277
+ @arg_calendar
278
+ @arg_output
279
+ def first(calendar: JoinedCalendars, output: ComponentsResult):
280
+ """Print only the first occurrence in each calendar.
281
+
282
+ \b
283
+ This example prints the first event in calendar.ics:
284
+ \b
285
+ ics-query first calendar.ics -
286
+
287
+ """ # noqa: D301
288
+ output.add_components(calendar.first())
289
+
290
+
291
+ @main.command()
292
+ @click.argument("start", type=parse.to_time)
293
+ @click.argument("end", type=parse.to_time_and_delta)
294
+ @arg_calendar
295
+ @arg_output
296
+ def between(
297
+ start: parse.Date,
298
+ end: parse.DateAndDelta,
299
+ calendar: JoinedCalendars,
300
+ output: ComponentsResult,
301
+ ):
302
+ """Print all occurrences between the START and the END.
303
+
304
+ The start is inclusive, the end is exclusive.
305
+
306
+ This example returns the events within the next week:
307
+
308
+ \b
309
+ ics-query between `date +%Y%m%d` +7d calendar.ics -
310
+
311
+ This example saves the events from the 1st of May 2024 to the 10th of June in
312
+ events.ics:
313
+
314
+ \b
315
+ ics-query between 2024-5-1 2024-6-10 calendar.ics events.ics
316
+
317
+ In this example, you can check what is happening on New Years Eve 2025 around
318
+ midnight:
319
+
320
+ \b
321
+ ics-query between 2025-12-31T21:00 +6h calendar.ics events.ics
322
+
323
+ \b
324
+ Absolute Time
325
+ -------------
326
+
327
+ START must be specified as an absolute time.
328
+ END can be absolute or relative to START, see Relative Time below.
329
+
330
+ Each of the formats specify the earliest time e.g. the start of a day.
331
+ Thus, if START == END, there are 0 seconds in between and the result is
332
+ only what happens during that time or starts exactly at that time.
333
+
334
+ YEAR
335
+
336
+ Specifiy the start of the year.
337
+
338
+ \b
339
+ Formats:
340
+ \b
341
+ YYYY
342
+ \b
343
+ Examples:
344
+ \b
345
+ 2024 # start of 2024
346
+ `date +%Y` # this year
347
+
348
+ MONTH
349
+
350
+ The start of the month.
351
+
352
+ \b
353
+ Formats:
354
+ \b
355
+ YYYY-MM
356
+ YYYY-M
357
+ YYYYMM
358
+ \b
359
+ Examples:
360
+ \b
361
+ 2019-10 # October 2019
362
+ 1990-01 # January 1990
363
+ 1990-1 # January 1990
364
+ 199001 # January 1990
365
+ `date +%Y%m` # this month
366
+
367
+ DAY
368
+
369
+ The start of the day
370
+
371
+ \b
372
+ Formats:
373
+ \b
374
+ YYYY-MM-DD
375
+ YYYY-M-D
376
+ YYYYMMDD
377
+ \b
378
+ Examples:
379
+ \b
380
+ 1990-01-01 # 1st January 1990
381
+ 1990-1-1 # 1st January 1990
382
+ 19900101 # 1st January 1990
383
+ `date +%Y%m%d` # today
384
+
385
+ HOUR
386
+
387
+ The start of the hour.
388
+
389
+ \b
390
+ Formats:
391
+ \b
392
+ YYYY-MM-DD HH
393
+ YYYY-MM-DDTHH
394
+ YYYY-M-DTH
395
+ YYYYMMDDTHH
396
+ YYYYMMDDHH
397
+ \b
398
+ Examples:
399
+ \b
400
+ 1990-01-01 01 # 1st January 1990, 1am
401
+ 1990-01-01T01 # 1st January 1990, 1am
402
+ 1990-1-1T17 # 1st January 1990, 17:00
403
+ 19900101T23 # 1st January 1990, 23:00
404
+ 1990010123 # 1st January 1990, 23:00
405
+ `date +%Y%m%d%H` # this hour
406
+
407
+ MINUTE
408
+
409
+ The start of a minute.
410
+
411
+ \b
412
+ Formats:
413
+ \b
414
+ YYYY-MM-DD HH:MM
415
+ YYYY-MM-DDTHH:MM
416
+ YYYY-M-DTH:M
417
+ YYYYMMDDTHHMM
418
+ YYYYMMDDHHMM
419
+ \b
420
+ Examples:
421
+ \b
422
+ 1990-01-01 10:10 # 1st January 1990, 10:10am
423
+ 1990-01-01T10:10 # 1st January 1990, 10:10am
424
+ 1990-1-1T7:2 # 1st January 1990, 07:02
425
+ 19900101T2359 # 1st January 1990, 23:59
426
+ 199001012359 # 1st January 1990, 23:59
427
+ `date +%Y%m%d%H%M` # this minute
428
+
429
+ SECOND
430
+
431
+ A precise time. RFC 5545 calendars are specified to the second.
432
+ This is the most precise format to specify times.
433
+
434
+ \b
435
+ Formats:
436
+ \b
437
+ YYYY-MM-DD HH:MM:SS
438
+ YYYY-MM-DDTHH:MM:SS
439
+ YYYY-M-DTH:M:S
440
+ YYYYMMDDTHHMMSS
441
+ YYYYMMDDHHMMSS
442
+ \b
443
+ Examples:
444
+ \b
445
+ 1990-01-01 10:10:00 # 1st January 1990, 10:10am
446
+ 1990-01-01T10:10:00 # 1st January 1990, 10:10am
447
+ 1990-1-1T7:2:30 # 1st January 1990, 07:02:30
448
+ 19901231T235959 # 31st December 1990, 23:59:59
449
+ 19900101235959 # 1st January 1990, 23:59:59
450
+ `date +%Y%m%d%H%M%S` # now
451
+ \b
452
+ Relative Time
453
+ -------------
454
+
455
+ The END argument can be a time range.
456
+ The + at the beginning is optional but makes for a better reading.
457
+
458
+ \b
459
+ Examples:
460
+ \b
461
+ Add 10 days to START: +10d
462
+ Add 24 hours to START: +1d or +24h
463
+ Add 3 hours to START: +3h
464
+ Add 30 minutes to START: +30m
465
+ Add 1000 seconds to START: +1000s
466
+ \b
467
+ You can also combine the ranges:
468
+ Add 1 day and 12 hours to START: +1d12h
469
+ Add 3 hours and 15 minutes to START: +3h15m
470
+
471
+ """ # noqa: D301
472
+ output.add_components(calendar.between(start, end))
258
473
 
259
474
 
260
475
  __all__ = ["main"]
@@ -2,9 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import datetime
5
6
  import re
7
+ from typing import Union
6
8
 
7
- DateArgument = tuple[int]
9
+ Date = tuple[int]
10
+ DateAndDelta = Union[Date, datetime.timedelta]
8
11
 
9
12
 
10
13
  class InvalidTimeFormat(ValueError):
@@ -21,14 +24,22 @@ REGEX_TIME = re.compile(
21
24
  r"$"
22
25
  )
23
26
 
27
+ REGEX_TIMEDELTA = re.compile(
28
+ r"^\+?(?:(?P<days>\d+)d)?"
29
+ r"(?:(?P<hours>\d+)h)?"
30
+ r"(?:(?P<minutes>\d+)m)?"
31
+ r"(?:(?P<seconds>\d+)s)?"
32
+ r"$"
33
+ )
34
+
24
35
 
25
- def to_time(dt: str) -> DateArgument:
36
+ def to_time(dt: str) -> Date:
26
37
  """Parse the time and date."""
27
38
  parsed_dt = REGEX_TIME.match(dt)
28
39
  if parsed_dt is None:
29
40
  raise InvalidTimeFormat(dt)
30
41
 
31
- def group(group_name: str) -> tuple[int]:
42
+ def group(group_name: str) -> Date:
32
43
  """Return a group's value."""
33
44
  result = parsed_dt.group(group_name)
34
45
  while result and result[0] not in "0123456789":
@@ -47,4 +58,15 @@ def to_time(dt: str) -> DateArgument:
47
58
  )
48
59
 
49
60
 
50
- __all__ = ["to_time", "DateArgument"]
61
+ def to_time_and_delta(dt: str) -> DateAndDelta:
62
+ """Parse to a absolute time or timedelta."""
63
+ parsed_td = REGEX_TIMEDELTA.match(dt)
64
+ if parsed_td is None:
65
+ return to_time(dt)
66
+ kw = {k: int(v) for k, v in parsed_td.groupdict().items() if v is not None}
67
+ if not kw:
68
+ raise InvalidTimeFormat(dt)
69
+ return datetime.timedelta(**kw)
70
+
71
+
72
+ __all__ = ["to_time", "Date", "to_time_and_delta", "DateAndDelta"]
@@ -0,0 +1,24 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:Work
3
+ DTSTART;TZID=Europe/Berlin:20240823T090000
4
+ DTEND;TZID=Europe/Berlin:20240823T170000
5
+ DTSTAMP:20240823T082915Z
6
+ UID:6b85b60c-eb1a-4338-9ece-33541b95bf17
7
+ SEQUENCE:1
8
+ CREATED:20240823T082829Z
9
+ LAST-MODIFIED:20240823T082915Z
10
+ TRANSP:OPAQUE
11
+ X-MOZ-GENERATION:2
12
+ END:VEVENT
13
+ BEGIN:VEVENT
14
+ SUMMARY:Work
15
+ DTSTART;TZID=Europe/Berlin:20240826T090000
16
+ DTEND;TZID=Europe/Berlin:20240826T170000
17
+ DTSTAMP:20240823T082915Z
18
+ UID:6b85b60c-eb1a-4338-9ece-33541b95bf17
19
+ SEQUENCE:1
20
+ CREATED:20240823T082829Z
21
+ LAST-MODIFIED:20240823T082915Z
22
+ TRANSP:OPAQUE
23
+ X-MOZ-GENERATION:2
24
+ END:VEVENT
@@ -0,0 +1,7 @@
1
+ BEGIN:VCALENDAR
2
+ VERSION:2.0
3
+ PRODID:-//SabreDAV//SabreDAV//EN
4
+ CALSCALE:GREGORIAN
5
+ X-WR-CALNAME:empty-calendar
6
+ X-APPLE-CALENDAR-COLOR:#e78074
7
+ END:VCALENDAR
@@ -0,0 +1,223 @@
1
+ BEGIN:VCALENDAR
2
+ PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
3
+ VERSION:2.0
4
+ BEGIN:VTIMEZONE
5
+ TZID:Europe/Berlin
6
+ X-TZINFO:Europe/Berlin[2024a]
7
+ BEGIN:STANDARD
8
+ TZOFFSETTO:+010000
9
+ TZOFFSETFROM:+005328
10
+ TZNAME:Europe/Berlin(STD)
11
+ DTSTART:18930401T000000
12
+ RDATE:18930401T000000
13
+ END:STANDARD
14
+ BEGIN:DAYLIGHT
15
+ TZOFFSETTO:+020000
16
+ TZOFFSETFROM:+010000
17
+ TZNAME:Europe/Berlin(DST)
18
+ DTSTART:19160430T230000
19
+ RDATE:19160430T230000
20
+ END:DAYLIGHT
21
+ BEGIN:STANDARD
22
+ TZOFFSETTO:+010000
23
+ TZOFFSETFROM:+020000
24
+ TZNAME:Europe/Berlin(STD)
25
+ DTSTART:19161001T010000
26
+ RDATE:19161001T010000
27
+ END:STANDARD
28
+ BEGIN:DAYLIGHT
29
+ TZOFFSETTO:+020000
30
+ TZOFFSETFROM:+010000
31
+ TZNAME:Europe/Berlin(DST)
32
+ DTSTART:19170416T020000
33
+ RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T020000
34
+ END:DAYLIGHT
35
+ BEGIN:STANDARD
36
+ TZOFFSETTO:+010000
37
+ TZOFFSETFROM:+020000
38
+ TZNAME:Europe/Berlin(STD)
39
+ DTSTART:19170917T030000
40
+ RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T030000
41
+ END:STANDARD
42
+ BEGIN:DAYLIGHT
43
+ TZOFFSETTO:+020000
44
+ TZOFFSETFROM:+010000
45
+ TZNAME:Europe/Berlin(DST)
46
+ DTSTART:19400401T020000
47
+ RDATE:19400401T020000
48
+ END:DAYLIGHT
49
+ BEGIN:STANDARD
50
+ TZOFFSETTO:+010000
51
+ TZOFFSETFROM:+020000
52
+ TZNAME:Europe/Berlin(STD)
53
+ DTSTART:19421102T030000
54
+ RDATE:19421102T030000
55
+ END:STANDARD
56
+ BEGIN:DAYLIGHT
57
+ TZOFFSETTO:+020000
58
+ TZOFFSETFROM:+010000
59
+ TZNAME:Europe/Berlin(DST)
60
+ DTSTART:19430329T020000
61
+ RDATE:19430329T020000
62
+ END:DAYLIGHT
63
+ BEGIN:DAYLIGHT
64
+ TZOFFSETTO:+020000
65
+ TZOFFSETFROM:+010000
66
+ TZNAME:Europe/Berlin(DST)
67
+ DTSTART:19440403T020000
68
+ RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T020000
69
+ END:DAYLIGHT
70
+ BEGIN:DAYLIGHT
71
+ TZOFFSETTO:+030000
72
+ TZOFFSETFROM:+020000
73
+ TZNAME:Europe/Berlin(DST)
74
+ DTSTART:19450524T020000
75
+ RDATE:19450524T020000
76
+ END:DAYLIGHT
77
+ BEGIN:STANDARD
78
+ TZOFFSETTO:+010000
79
+ TZOFFSETFROM:+020000
80
+ TZNAME:Europe/Berlin(STD)
81
+ DTSTART:19431004T030000
82
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1MO;UNTIL=19441002T030000
83
+ END:STANDARD
84
+ BEGIN:DAYLIGHT
85
+ TZOFFSETTO:+020000
86
+ TZOFFSETFROM:+030000
87
+ TZNAME:Europe/Berlin(DST)
88
+ DTSTART:19450924T030000
89
+ RDATE:19450924T030000
90
+ END:DAYLIGHT
91
+ BEGIN:STANDARD
92
+ TZOFFSETTO:+010000
93
+ TZOFFSETFROM:+020000
94
+ TZNAME:Europe/Berlin(STD)
95
+ DTSTART:19451118T030000
96
+ RDATE:19451118T030000
97
+ END:STANDARD
98
+ BEGIN:DAYLIGHT
99
+ TZOFFSETTO:+020000
100
+ TZOFFSETFROM:+010000
101
+ TZNAME:Europe/Berlin(DST)
102
+ DTSTART:19460414T020000
103
+ RDATE:19460414T020000
104
+ END:DAYLIGHT
105
+ BEGIN:DAYLIGHT
106
+ TZOFFSETTO:+020000
107
+ TZOFFSETFROM:+010000
108
+ TZNAME:Europe/Berlin(DST)
109
+ DTSTART:19470406T030000
110
+ RDATE:19470406T030000
111
+ END:DAYLIGHT
112
+ BEGIN:DAYLIGHT
113
+ TZOFFSETTO:+030000
114
+ TZOFFSETFROM:+020000
115
+ TZNAME:Europe/Berlin(DST)
116
+ DTSTART:19470511T030000
117
+ RDATE:19470511T030000
118
+ END:DAYLIGHT
119
+ BEGIN:STANDARD
120
+ TZOFFSETTO:+010000
121
+ TZOFFSETFROM:+020000
122
+ TZNAME:Europe/Berlin(STD)
123
+ DTSTART:19461007T030000
124
+ RDATE:19461007T030000
125
+ END:STANDARD
126
+ BEGIN:DAYLIGHT
127
+ TZOFFSETTO:+020000
128
+ TZOFFSETFROM:+030000
129
+ TZNAME:Europe/Berlin(DST)
130
+ DTSTART:19470629T030000
131
+ RDATE:19470629T030000
132
+ END:DAYLIGHT
133
+ BEGIN:DAYLIGHT
134
+ TZOFFSETTO:+020000
135
+ TZOFFSETFROM:+010000
136
+ TZNAME:Europe/Berlin(DST)
137
+ DTSTART:19480418T020000
138
+ RDATE:19480418T020000
139
+ END:DAYLIGHT
140
+ BEGIN:DAYLIGHT
141
+ TZOFFSETTO:+020000
142
+ TZOFFSETFROM:+010000
143
+ TZNAME:Europe/Berlin(DST)
144
+ DTSTART:19490410T020000
145
+ RDATE:19490410T020000
146
+ END:DAYLIGHT
147
+ BEGIN:STANDARD
148
+ TZOFFSETTO:+010000
149
+ TZOFFSETFROM:+020000
150
+ TZNAME:Europe/Berlin(STD)
151
+ DTSTART:19471005T030000
152
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T030000
153
+ END:STANDARD
154
+ BEGIN:DAYLIGHT
155
+ TZOFFSETTO:+020000
156
+ TZOFFSETFROM:+010000
157
+ TZNAME:Europe/Berlin(DST)
158
+ DTSTART:19800406T020000
159
+ RDATE:19800406T020000
160
+ END:DAYLIGHT
161
+ BEGIN:STANDARD
162
+ TZOFFSETTO:+010000
163
+ TZOFFSETFROM:+020000
164
+ TZNAME:Europe/Berlin(STD)
165
+ DTSTART:19800928T030000
166
+ RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T030000
167
+ END:STANDARD
168
+ BEGIN:DAYLIGHT
169
+ TZOFFSETTO:+020000
170
+ TZOFFSETFROM:+010000
171
+ TZNAME:Europe/Berlin(DST)
172
+ DTSTART:19810329T020000
173
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19960331T020000
174
+ END:DAYLIGHT
175
+ BEGIN:STANDARD
176
+ TZOFFSETTO:+010000
177
+ TZOFFSETFROM:+020000
178
+ TZNAME:Europe/Berlin(STD)
179
+ DTSTART:19961027T030000
180
+ RDATE:19961027T030000
181
+ END:STANDARD
182
+ BEGIN:DAYLIGHT
183
+ TZOFFSETTO:+020000
184
+ TZOFFSETFROM:+010000
185
+ TZNAME:(DST)
186
+ DTSTART:19970330T020000
187
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
188
+ END:DAYLIGHT
189
+ BEGIN:STANDARD
190
+ TZOFFSETTO:+010000
191
+ TZOFFSETFROM:+020000
192
+ TZNAME:(STD)
193
+ DTSTART:19971026T030000
194
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
195
+ END:STANDARD
196
+ END:VTIMEZONE
197
+ BEGIN:VEVENT
198
+ CREATED:20240823T082735Z
199
+ LAST-MODIFIED:20240823T082802Z
200
+ DTSTAMP:20240823T082802Z
201
+ UID:22d43072-b75a-43da-bed0-a5da8a7a6853
202
+ SUMMARY:Weekly Tuesday Morning Meeting
203
+ RRULE:FREQ=WEEKLY
204
+ DTSTART;TZID=Europe/Berlin:20240820T090000
205
+ DTEND;TZID=Europe/Berlin:20240820T100000
206
+ TRANSP:OPAQUE
207
+ X-MOZ-GENERATION:2
208
+ SEQUENCE:1
209
+ END:VEVENT
210
+ BEGIN:VEVENT
211
+ CREATED:20240823T082829Z
212
+ LAST-MODIFIED:20240823T082915Z
213
+ DTSTAMP:20240823T082915Z
214
+ UID:6b85b60c-eb1a-4338-9ece-33541b95bf17
215
+ SUMMARY:Work
216
+ RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR
217
+ DTSTART;TZID=Europe/Berlin:20240819T090000
218
+ DTEND;TZID=Europe/Berlin:20240819T170000
219
+ TRANSP:OPAQUE
220
+ X-MOZ-GENERATION:2
221
+ SEQUENCE:1
222
+ END:VEVENT
223
+ END:VCALENDAR
@@ -0,0 +1,12 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:Work
3
+ DTSTART;TZID=Europe/Berlin:20240819T090000
4
+ DTEND;TZID=Europe/Berlin:20240819T170000
5
+ DTSTAMP:20240823T082915Z
6
+ UID:6b85b60c-eb1a-4338-9ece-33541b95bf17
7
+ SEQUENCE:1
8
+ CREATED:20240823T082829Z
9
+ LAST-MODIFIED:20240823T082915Z
10
+ TRANSP:OPAQUE
11
+ X-MOZ-GENERATION:2
12
+ END:VEVENT
@@ -2,7 +2,7 @@
2
2
 
3
3
  import pytest
4
4
 
5
- from ics_query.parse import InvalidTimeFormat, to_time
5
+ from ics_query.parse import InvalidTimeFormat, to_time, to_time_and_delta
6
6
 
7
7
 
8
8
  @pytest.mark.parametrize(
@@ -43,9 +43,10 @@ from ics_query.parse import InvalidTimeFormat, to_time
43
43
  ("20141010183012", (2014, 10, 10, 18, 30, 12)),
44
44
  ],
45
45
  )
46
- def test_parse_to_date_argument(string_argument, expected_result):
46
+ @pytest.mark.parametrize("parser", [to_time_and_delta, to_time])
47
+ def test_parse_to_date_argument(string_argument, expected_result, parser):
47
48
  """Check that we can properly parse what is accepted."""
48
- result = to_time(string_argument)
49
+ result = parser(string_argument)
49
50
  assert result == expected_result
50
51
 
51
52
 
@@ -53,11 +54,13 @@ def test_parse_to_date_argument(string_argument, expected_result):
53
54
  "dt",
54
55
  [
55
56
  "",
57
+ " ",
56
58
  "132",
57
59
  "12345",
58
60
  ],
59
61
  )
60
- def test_invalid_time_format(dt: str):
62
+ @pytest.mark.parametrize("parser", [to_time_and_delta, to_time])
63
+ def test_invalid_time_format(dt: str, parser):
61
64
  """Check invalid time formats."""
62
65
  with pytest.raises(InvalidTimeFormat):
63
- to_time(dt)
66
+ parser(dt)
@@ -0,0 +1,25 @@
1
+ """This tests parsing input times and dates."""
2
+
3
+ from datetime import timedelta
4
+
5
+ import pytest
6
+
7
+ from ics_query.parse import to_time_and_delta
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ ("string_argument", "expected_result"),
12
+ [
13
+ ("10d", timedelta(days=10)),
14
+ ("10d10h", timedelta(days=10, hours=10)),
15
+ ("1d2h12m33s", timedelta(days=1, hours=2, minutes=12, seconds=33)),
16
+ ("3600s", timedelta(seconds=3600)),
17
+ ("10m", timedelta(minutes=10)),
18
+ ("23h", timedelta(hours=23)),
19
+ ],
20
+ )
21
+ @pytest.mark.parametrize("plus", ["", "+"])
22
+ def test_parse_to_date_argument(string_argument, expected_result, plus):
23
+ """Check that we can properly parse what is accepted."""
24
+ result = to_time_and_delta(plus + string_argument)
25
+ assert result == expected_result
@@ -15,7 +15,7 @@ maintainers = [
15
15
  ]
16
16
  description = "Find out what happens in ICS calendar files - query and filter RFC 5545 compatible .ics files for events, journals, TODOs and more."
17
17
  readme = "README.md"
18
- requires-python = ">=3.8"
18
+ requires-python = ">=3.9"
19
19
  # see https://pypi.python.org/pypi?%3Aaction=list_classifiers
20
20
  classifiers = [
21
21
  "Development Status :: 3 - Alpha",
@@ -31,7 +31,7 @@ classifiers = [
31
31
 
32
32
  dependencies = [
33
33
  "icalendar",
34
- "recurring-ical-events",
34
+ "recurring-ical-events>=3.2.0,<4",
35
35
  "click",
36
36
  "icalendar",
37
37
  ]
@@ -4,10 +4,12 @@
4
4
  # and then run "tox" from this directory.
5
5
 
6
6
  [tox]
7
+ skipsdist = True
7
8
  envlist = py39, py310, py311, py312, ruff
8
9
 
9
10
  [testenv]
10
- deps = pytest
11
+ deps =
12
+ -e {tox_root}[test]
11
13
  setenv = TMPDIR={envtmpdir}
12
14
  commands =
13
15
  pytest --basetemp="{envtmpdir}" {posargs}
File without changes
File without changes
File without changes