ics-query 0.4.24__py3-none-any.whl → 0.4.33__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 (25) hide show
  1. ics_query/_version.py +2 -2
  2. ics_query/cli.py +98 -14
  3. ics_query/tests/conftest.py +17 -9
  4. ics_query/tests/runs/all --tz Singapore one-event.ics -.run +1 -0
  5. ics_query/tests/runs/all three-events.ics -.run +3 -0
  6. ics_query/tests/runs/at 2014-05-03 x-wr-timezone-rdate-hackerpublicradio.ics -.run +9 -0
  7. ics_query/tests/runs/at 2019-03-04 multiple-calendars.ics -.run +2 -0
  8. ics_query/tests/runs/at 2019-03-04 one-event-twice.ics -.run +2 -0
  9. ics_query/tests/runs/at 2019-03-04 one-event.ics -.run +1 -0
  10. ics_query/tests/runs/at 2019-03-07 multiple-calendars.ics -.run +1 -0
  11. ics_query/tests/runs/at 2024-08-20 Berlin-Los-Angeles.ics -.run +2 -0
  12. ics_query/tests/runs/between 20240823 4d recurring-work-events.ics -.run +2 -0
  13. ics_query/tests/runs/calendars/x-wr-timezone-rdate-hackerpublicradio.ics +26 -0
  14. ics_query/tests/runs/first -c VJOURNAL -c VEVENT one-event.ics -.run +1 -0
  15. ics_query/tests/runs/first -c VJOURNAL simple-journal.ics -.run +1 -0
  16. ics_query/tests/runs/first -c VTODO -c VJOURNAL simple-todo.ics -.run +1 -0
  17. ics_query/tests/runs/first -c VTODO simple-todo.ics -.run +1 -0
  18. ics_query/tests/runs/first recurring-work-events.ics -.run +1 -0
  19. ics_query/tests/test_command_line.py +8 -2
  20. ics_query/tests/test_issue_40_valid_calendar.py +68 -0
  21. {ics_query-0.4.24.dist-info → ics_query-0.4.33.dist-info}/METADATA +48 -5
  22. {ics_query-0.4.24.dist-info → ics_query-0.4.33.dist-info}/RECORD +25 -22
  23. {ics_query-0.4.24.dist-info → ics_query-0.4.33.dist-info}/WHEEL +0 -0
  24. {ics_query-0.4.24.dist-info → ics_query-0.4.33.dist-info}/entry_points.txt +0 -0
  25. {ics_query-0.4.24.dist-info → ics_query-0.4.33.dist-info}/licenses/LICENSE +0 -0
ics_query/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.4.24'
21
- __version_tuple__ = version_tuple = (0, 4, 24)
20
+ __version__ = version = '0.4.33'
21
+ __version_tuple__ = version_tuple = (0, 4, 33)
ics_query/cli.py CHANGED
@@ -24,18 +24,18 @@ import typing as t
24
24
  import zoneinfo
25
25
 
26
26
  import click
27
- from icalendar.cal import Calendar, Component
27
+ from icalendar import Calendar, Component, Timezone
28
28
  from tzlocal import get_localzone_name
29
29
 
30
30
  from . import parse
31
31
  from .query import Query
32
- from .version import cli_version
32
+ from .version import __version__, cli_version
33
33
 
34
34
  if t.TYPE_CHECKING:
35
35
  from io import FileIO
36
36
 
37
37
  import recurring_ical_events
38
- from icalendar.cal import Component
38
+ from icalendar import Component
39
39
 
40
40
  from .parse import Date
41
41
 
@@ -67,17 +67,58 @@ class ComponentsResult:
67
67
  def __init__(self, output: FileIO):
68
68
  """Create a new result."""
69
69
  self._file = output
70
+ self._entered = False
70
71
 
71
- def add_component(self, component: Component):
72
+ def write(self, data: bytes | str):
73
+ """Write data to the output."""
74
+ if isinstance(data, str):
75
+ data = data.encode()
76
+ self._file.write(data)
77
+
78
+ def __enter__(self):
79
+ """Start adding components."""
80
+ self._entered = True
81
+
82
+ def __exit__(self, exc_type, exc_value, traceback):
83
+ """Stop adding components."""
84
+ self._entered = False
85
+
86
+ def add_component(self, component: Component) -> None:
72
87
  """Return a component."""
73
- self._file.write(component.to_ical())
88
+ assert self._entered
89
+ self.write(component.to_ical())
74
90
 
75
- def add_components(self, components: t.Iterable[Component]):
91
+ def add_components(self, components: t.Iterable[Component]) -> None:
76
92
  """Add all components."""
77
93
  for component in components:
78
94
  self.add_component(component)
79
95
 
80
96
 
97
+ class CalendarResult(ComponentsResult):
98
+ """Wrap the resulting components in a calendar."""
99
+
100
+ CALENDAR_START = (
101
+ f"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-query {__version__}\r\n"
102
+ )
103
+ CALENDAR_END = "END:VCALENDAR\r\n"
104
+
105
+ def __init__(self, output: FileIO, timezones: list[Timezone]):
106
+ super().__init__(output)
107
+ self.timezones = timezones
108
+
109
+ def __enter__(self):
110
+ """Start the calendar."""
111
+ super().__enter__()
112
+ self.write(self.CALENDAR_START)
113
+ for timezone in self.timezones:
114
+ self.write(timezone.to_ical())
115
+
116
+ def __exit__(self, exc_type, exc_value, traceback):
117
+ """Stop the calendar."""
118
+ self.write(self.CALENDAR_END)
119
+ super().__exit__(exc_type, exc_value, traceback)
120
+
121
+
81
122
  class ComponentsResultArgument(click.File):
82
123
  """Argument for the result."""
83
124
 
@@ -89,6 +130,11 @@ class ComponentsResultArgument(click.File):
89
130
  ) -> ComponentsResult:
90
131
  """Return a ComponentsResult."""
91
132
  file = super().convert(value, param, ctx)
133
+ # we claim the as_calendar argument
134
+ wrap_calendar = ctx.params.pop("as_calendar", False)
135
+ if wrap_calendar:
136
+ joined: JoinedCalendars = ctx.params["calendar"]
137
+ return CalendarResult(file, joined.timezones)
92
138
  return ComponentsResult(file)
93
139
 
94
140
 
@@ -97,6 +143,7 @@ class JoinedCalendars:
97
143
  self, calendars: list[Calendar], timezone: str, components: t.Sequence[str]
98
144
  ):
99
145
  """Join multiple calendars."""
146
+ self.calendars = calendars
100
147
  self.queries = [
101
148
  Query(calendar, timezone, components=components) for calendar in calendars
102
149
  ]
@@ -124,6 +171,27 @@ class JoinedCalendars:
124
171
  for query in self.queries:
125
172
  yield from query.between(start, end)
126
173
 
174
+ @property
175
+ def timezones(self) -> list[Timezone]:
176
+ """Return all the timezones in use."""
177
+ result = []
178
+ tzids = set()
179
+ # add existing timezone components first
180
+ for calendar in self.calendars:
181
+ for timezone in calendar.timezones:
182
+ if timezone.tz_name not in tzids:
183
+ tzids.add(timezone.tz_name)
184
+ result.append(timezone)
185
+ # add X-WR-TIMEZONE later to prevent generating if existing
186
+ for calendar in self.calendars:
187
+ tzid = calendar.get("X-WR-TIMEZONE", None)
188
+ if tzid is not None and tzid not in tzids:
189
+ timezone = Timezone.from_tzid(tzid)
190
+ tzids.add(timezone.tz_name)
191
+ tzids.add(tzid)
192
+ result.append(timezone)
193
+ return result
194
+
127
195
 
128
196
  class CalendarQueryInputArgument(click.File):
129
197
  """Argument for the result."""
@@ -160,6 +228,15 @@ opt_timezone = click.option(
160
228
  help=("Set the timezone. See also --available-timezones"),
161
229
  )
162
230
 
231
+ opt_calendar = click.option(
232
+ "--as-calendar",
233
+ envvar=ENV_PREFIX + "_AS_CALENDAR",
234
+ is_flag=True,
235
+ default=False,
236
+ is_eager=True,
237
+ help="Return a valid calendar, not just the components.",
238
+ )
239
+
163
240
 
164
241
  def arg_calendar(func):
165
242
  """Decorator for a calendar argument with all used options."""
@@ -173,9 +250,12 @@ def arg_calendar(func):
173
250
  return opt_timezone(opt_components(arg(wrapper)))
174
251
 
175
252
 
176
- arg_output = click.argument("output", type=ComponentsResultArgument("wb"), default="-")
177
- # Option with many values and list as result
178
- # see https://click.palletsprojects.com/en/latest/options/#multiple-options
253
+ def arg_output(func):
254
+ """Add the output argument and its parameters."""
255
+ # Option with many values and list as result
256
+ # see https://click.palletsprojects.com/en/latest/options/#multiple-options
257
+ arg = click.argument("output", type=ComponentsResultArgument("wb"), default="-")
258
+ return opt_calendar(arg(func))
179
259
 
180
260
 
181
261
  def opt_available_timezones(*param_decls: str, **kwargs: t.Any) -> t.Callable:
@@ -210,7 +290,7 @@ def opt_available_timezones(*param_decls: str, **kwargs: t.Any) -> t.Callable:
210
290
 
211
291
 
212
292
  def opt_license(*param_decls: str, **kwargs: t.Any) -> t.Callable:
213
- """List available timezones.
293
+ """Show the license
214
294
 
215
295
  This is copied from the --help option.
216
296
  """
@@ -458,7 +538,8 @@ def at(calendar: JoinedCalendars, output: ComponentsResult, date: Date):
458
538
  ics-query at 19900101235959 # 1st January 1990, 23:59:59
459
539
  ics-query at `date +%Y%m%d%H%M%S` # now
460
540
  """ # noqa: D301
461
- output.add_components(calendar.at(date))
541
+ with output:
542
+ output.add_components(calendar.at(date))
462
543
 
463
544
 
464
545
  @cli.command()
@@ -475,7 +556,8 @@ def first(calendar: JoinedCalendars, output: ComponentsResult):
475
556
  ics-query first --component VEVENT calendar.ics -
476
557
 
477
558
  """ # noqa: D301
478
- output.add_components(calendar.first())
559
+ with output:
560
+ output.add_components(calendar.first())
479
561
 
480
562
 
481
563
  @cli.command()
@@ -499,7 +581,8 @@ def all(calendar: JoinedCalendars, output: ComponentsResult): # noqa: A001
499
581
  potentially enourmous. You can mitigate this by closing the OUTPUT
500
582
  when you have enough e.g. with a head command.
501
583
  """ # noqa: D301
502
- output.add_components(calendar.all())
584
+ with output:
585
+ output.add_components(calendar.all())
503
586
 
504
587
 
505
588
  @cli.command()
@@ -683,7 +766,8 @@ def between(
683
766
  Add 3 hours and 15 minutes to START: +3h15m
684
767
 
685
768
  """ # noqa: D301
686
- output.add_components(calendar.between(start, end))
769
+ with output:
770
+ output.add_components(calendar.between(start, end))
687
771
 
688
772
 
689
773
  def main():
@@ -22,13 +22,14 @@ from pathlib import Path
22
22
  from typing import Callable, NamedTuple
23
23
 
24
24
  import pytest
25
+ from icalendar import Calendar
25
26
 
26
27
  HERE = Path(__file__).parent
27
28
  IO_DIRECTORY = HERE / "runs"
28
29
  CALENDARS_DIRECTORY = IO_DIRECTORY / "calendars"
29
30
 
30
31
 
31
- class TestRun(NamedTuple):
32
+ class ExampleRun(NamedTuple):
32
33
  """The result from a test run."""
33
34
 
34
35
  exit_code: int
@@ -38,7 +39,7 @@ class TestRun(NamedTuple):
38
39
  @classmethod
39
40
  def from_completed_process(
40
41
  cls, completed_process: subprocess.CompletedProcess
41
- ) -> TestRun:
42
+ ) -> ExampleRun:
42
43
  """Create a new run result."""
43
44
  stdout = completed_process.stdout.decode("UTF-8").replace("\r\n", "\n")
44
45
  print(stdout)
@@ -48,6 +49,11 @@ class TestRun(NamedTuple):
48
49
  completed_process.stderr.decode("UTF-8"),
49
50
  )
50
51
 
52
+ @property
53
+ def calendar(self) -> Calendar:
54
+ """Return the output as a calendar."""
55
+ return Calendar.from_ical(self.output)
56
+
51
57
 
52
58
  def get_binary_path(request: pytest.FixtureRequest) -> str:
53
59
  """Return the path to the ics-query command."""
@@ -59,7 +65,7 @@ def get_binary_path(request: pytest.FixtureRequest) -> str:
59
65
  return Path(command).absolute()
60
66
 
61
67
 
62
- def run_ics_query(*command, cwd=CALENDARS_DIRECTORY, binary: str) -> TestRun:
68
+ def run_ics_query(*command, cwd=CALENDARS_DIRECTORY, binary: str) -> ExampleRun:
63
69
  """Run ics-qeury with a command.
64
70
 
65
71
  - cwd is the working directory
@@ -74,7 +80,9 @@ def run_ics_query(*command, cwd=CALENDARS_DIRECTORY, binary: str) -> TestRun:
74
80
  check=False,
75
81
  cwd=cwd,
76
82
  )
77
- return TestRun.from_completed_process(completed_process)
83
+ if completed_process.stderr:
84
+ print(completed_process.stderr.decode())
85
+ return ExampleRun.from_completed_process(completed_process)
78
86
 
79
87
 
80
88
  class IOTestCase(NamedTuple):
@@ -92,7 +100,7 @@ class IOTestCase(NamedTuple):
92
100
  expected_output = path.read_text(encoding="UTF-8").replace("\r\n", "\n")
93
101
  return cls(path.name, path.stem.split(), path.parent, expected_output, binary)
94
102
 
95
- def run(self) -> TestRun:
103
+ def run(self) -> ExampleRun:
96
104
  """Run this test case and return the result."""
97
105
  return run_ics_query(*self.command, binary=self.binary)
98
106
 
@@ -100,7 +108,7 @@ class IOTestCase(NamedTuple):
100
108
  io_test_case_paths = [
101
109
  test_case_path
102
110
  for test_case_path in IO_DIRECTORY.iterdir()
103
- if test_case_path.is_file()
111
+ if test_case_path.is_file() and test_case_path.suffix == ".run"
104
112
  ]
105
113
 
106
114
 
@@ -112,8 +120,8 @@ def io_testcase(request: pytest.FixtureRequest) -> IOTestCase:
112
120
  return IOTestCase.from_path(path, binary)
113
121
 
114
122
 
115
- @pytest.fixture
116
- def run(request: pytest.FixtureRequest) -> Callable[..., TestRun]:
123
+ @pytest.fixture()
124
+ def run(request: pytest.FixtureRequest) -> Callable[..., ExampleRun]:
117
125
  """Return a runner function."""
118
126
 
119
127
  def run(*args, **kw):
@@ -123,4 +131,4 @@ def run(request: pytest.FixtureRequest) -> Callable[..., TestRun]:
123
131
  return run
124
132
 
125
133
 
126
- __all__ = ["IOTestCase", "TestRun"]
134
+ __all__ = ["IOTestCase", "ExampleRun"]
@@ -4,6 +4,7 @@ DTSTART;TZID=Singapore:20190304T150000
4
4
  DTEND;TZID=Singapore:20190304T153000
5
5
  DTSTAMP:20190303T111937
6
6
  UID:UYDQSG9TH4DE0WM3QFL2J
7
+ RECURRENCE-ID;TZID=Singapore:20190304T150000
7
8
  CREATED:20190303T111937
8
9
  LAST-MODIFIED:20190303T111937
9
10
  END:VEVENT
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20190304T000000
4
4
  DTEND;TZID=Europe/Berlin:20190304T010000
5
5
  DTSTAMP:20190303T111937
6
6
  UID:UYDQSG9TH4DE0WM3QFL2J
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20190304T000000
7
8
  CLASS:PUBLIC
8
9
  CREATED:20190303T111937
9
10
  LAST-MODIFIED:20190303T111937
@@ -15,6 +16,7 @@ DTSTART;TZID=Europe/Berlin:20190307T000000
15
16
  DTEND;TZID=Europe/Berlin:20190307T010000
16
17
  DTSTAMP:20190303T111937
17
18
  UID:UYDQSG9TH4DE0WM3QFL2J
19
+ RECURRENCE-ID;TZID=Europe/Berlin:20190307T000000
18
20
  CLASS:PUBLIC
19
21
  CREATED:20190303T111937
20
22
  LAST-MODIFIED:20190303T111937
@@ -26,6 +28,7 @@ DTSTART;TZID=Europe/Berlin:20190310T000000
26
28
  DTEND;TZID=Europe/Berlin:20190310T010000
27
29
  DTSTAMP:20190303T111937
28
30
  UID:UYDQSG9TH4DE0WM3QFL2J
31
+ RECURRENCE-ID;TZID=Europe/Berlin:20190310T000000
29
32
  CLASS:PUBLIC
30
33
  CREATED:20190303T111937
31
34
  LAST-MODIFIED:20190303T111937
@@ -0,0 +1,9 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:HPR Community News
3
+ DTSTART;TZID=Europe/London:20140503T200000
4
+ DTEND;TZID=Europe/London:20140503T220000
5
+ RECURRENCE-ID;TZID=Europe/London:20140503T200000
6
+ DESCRIPTION:This is from http://www.hackerpublicradio.org/eps/hpr1286/iCal
7
+ endar_Hacking_shownotes.html
8
+ LOCATION:mumble.openspeak.cc port: 64747
9
+ END:VEVENT
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20190304T000000
4
4
  DTEND;TZID=Europe/Berlin:20190304T010000
5
5
  DTSTAMP:20190303T111937
6
6
  UID:UYDQSG9TH4DE0WM3QFL2J
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20190304T000000
7
8
  CLASS:PUBLIC
8
9
  CREATED:20190303T111937
9
10
  LAST-MODIFIED:20190303T111937
@@ -15,6 +16,7 @@ DTSTART;TZID=Europe/Berlin:20190304T080000
15
16
  DTEND;TZID=Europe/Berlin:20190304T083000
16
17
  DTSTAMP:20190303T111937
17
18
  UID:UYDQSG9TH4DE0WM3QFL2J
19
+ RECURRENCE-ID;TZID=Europe/Berlin:20190304T080000
18
20
  CREATED:20190303T111937
19
21
  LAST-MODIFIED:20190303T111937
20
22
  END:VEVENT
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20190304T080000
4
4
  DTEND;TZID=Europe/Berlin:20190304T083000
5
5
  DTSTAMP:20190303T111937
6
6
  UID:UYDQSG9TH4DE0WM3QFL2J
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20190304T080000
7
8
  CREATED:20190303T111937
8
9
  LAST-MODIFIED:20190303T111937
9
10
  END:VEVENT
@@ -13,6 +14,7 @@ DTSTART;TZID=Europe/Berlin:20190304T080000
13
14
  DTEND;TZID=Europe/Berlin:20190304T083000
14
15
  DTSTAMP:20190303T111937
15
16
  UID:UYDQSG9TH4DE0WM3QFL2J
17
+ RECURRENCE-ID;TZID=Europe/Berlin:20190304T080000
16
18
  CREATED:20190303T111937
17
19
  LAST-MODIFIED:20190303T111937
18
20
  END:VEVENT
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20190304T080000
4
4
  DTEND;TZID=Europe/Berlin:20190304T083000
5
5
  DTSTAMP:20190303T111937
6
6
  UID:UYDQSG9TH4DE0WM3QFL2J
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20190304T080000
7
8
  CREATED:20190303T111937
8
9
  LAST-MODIFIED:20190303T111937
9
10
  END:VEVENT
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20190307T000000
4
4
  DTEND;TZID=Europe/Berlin:20190307T010000
5
5
  DTSTAMP:20190303T111937
6
6
  UID:UYDQSG9TH4DE0WM3QFL2J
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20190307T000000
7
8
  CLASS:PUBLIC
8
9
  CREATED:20190303T111937
9
10
  LAST-MODIFIED:20190303T111937
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20240820T060000
4
4
  DTEND;TZID=Europe/Berlin:20240820T070000
5
5
  DTSTAMP:20240823T130444Z
6
6
  UID:b27ea261-f23d-4e03-a7ac-f8cb0d00f07f
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20240820T060000
7
8
  CREATED:20240823T120639Z
8
9
  LAST-MODIFIED:20240823T130444Z
9
10
  TRANSP:OPAQUE
@@ -15,6 +16,7 @@ DTSTART;TZID=America/Los_Angeles:20240820T060000
15
16
  DTEND;TZID=America/Los_Angeles:20240820T070000
16
17
  DTSTAMP:20240823T130711Z
17
18
  UID:6d7ff7f3-4bc4-4d89-afa0-771bd690518a
19
+ RECURRENCE-ID;TZID=America/Los_Angeles:20240820T060000
18
20
  SEQUENCE:1
19
21
  CREATED:20240823T120639Z
20
22
  LAST-MODIFIED:20240823T130711Z
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20240823T090000
4
4
  DTEND;TZID=Europe/Berlin:20240823T170000
5
5
  DTSTAMP:20240823T082915Z
6
6
  UID:6b85b60c-eb1a-4338-9ece-33541b95bf17
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20240823T090000
7
8
  SEQUENCE:1
8
9
  CREATED:20240823T082829Z
9
10
  LAST-MODIFIED:20240823T082915Z
@@ -16,6 +17,7 @@ DTSTART;TZID=Europe/Berlin:20240826T090000
16
17
  DTEND;TZID=Europe/Berlin:20240826T170000
17
18
  DTSTAMP:20240823T082915Z
18
19
  UID:6b85b60c-eb1a-4338-9ece-33541b95bf17
20
+ RECURRENCE-ID;TZID=Europe/Berlin:20240826T090000
19
21
  SEQUENCE:1
20
22
  CREATED:20240823T082829Z
21
23
  LAST-MODIFIED:20240823T082915Z
@@ -0,0 +1,26 @@
1
+ BEGIN:VCALENDAR
2
+ VERSION:2.0
3
+ PRODID:Data::ICal 0.20
4
+ X-WR-CALNAME:Hacker Public Radio
5
+ X-WR-TIMEZONE:Europe/London
6
+ BEGIN:VEVENT
7
+ DESCRIPTION:This is from http://www.hackerpublicradio.org/eps/hpr1286/iCalendar_
8
+ Hacking_shownotes.html
9
+ DTEND:20130803T210000Z
10
+ DTSTART:20130803T190000Z
11
+ LOCATION:mumble.openspeak.cc port: 64747
12
+ RDATE;VALUE=DATE-TIME:20130803T190000Z
13
+ RDATE;VALUE=DATE-TIME:20130831T190000Z
14
+ RDATE;VALUE=DATE-TIME:20131005T190000Z
15
+ RDATE;VALUE=DATE-TIME:20131102T190000Z
16
+ RDATE;VALUE=DATE-TIME:20131130T190000Z
17
+ RDATE;VALUE=DATE-TIME:20140104T190000Z
18
+ RDATE;VALUE=DATE-TIME:20140201T190000Z
19
+ RDATE;VALUE=DATE-TIME:20140301T190000Z
20
+ RDATE;VALUE=DATE-TIME:20140405T190000Z
21
+ RDATE;VALUE=DATE-TIME:20140503T190000Z
22
+ RDATE;VALUE=DATE-TIME:20140531T190000Z
23
+ RDATE;VALUE=DATE-TIME:20140705T190000Z
24
+ SUMMARY:HPR Community News
25
+ END:VEVENT
26
+ END:VCALENDAR
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20190304T080000
4
4
  DTEND;TZID=Europe/Berlin:20190304T083000
5
5
  DTSTAMP:20190303T111937
6
6
  UID:UYDQSG9TH4DE0WM3QFL2J
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20190304T080000
7
8
  CREATED:20190303T111937
8
9
  LAST-MODIFIED:20190303T111937
9
10
  END:VEVENT
@@ -7,6 +7,7 @@ DESCRIPTION:We made it this year too. Probably. What's the point of a re
7
7
  DTSTAMP:19920901T130000Z
8
8
  DTSTART;VALUE=DATE:19920420
9
9
  PRIORITY:1
10
+ RECURRENCE-ID;VALUE=DATE:19920420
10
11
  SUMMARY:Yearly Income Tax Report
11
12
  UID:19920901T130000Z-123409@host.com
12
13
  END:VJOURNAL
@@ -5,6 +5,7 @@ DTSTAMP:19920901T130000Z
5
5
  DTSTART:19920415T133000Z
6
6
  DUE:19920516T045959Z
7
7
  PRIORITY:1
8
+ RECURRENCE-ID:19920415T133000Z
8
9
  SUMMARY:Yearly Income Tax Preparation
9
10
  UID:19920901T130000Z-123408@host.com
10
11
  END:VTODO
@@ -5,6 +5,7 @@ DTSTAMP:19920901T130000Z
5
5
  DTSTART:19920415T133000Z
6
6
  DUE:19920516T045959Z
7
7
  PRIORITY:1
8
+ RECURRENCE-ID:19920415T133000Z
8
9
  SUMMARY:Yearly Income Tax Preparation
9
10
  UID:19920901T130000Z-123408@host.com
10
11
  END:VTODO
@@ -4,6 +4,7 @@ DTSTART;TZID=Europe/Berlin:20240819T090000
4
4
  DTEND;TZID=Europe/Berlin:20240819T170000
5
5
  DTSTAMP:20240823T082915Z
6
6
  UID:6b85b60c-eb1a-4338-9ece-33541b95bf17
7
+ RECURRENCE-ID;TZID=Europe/Berlin:20240819T090000
7
8
  SEQUENCE:1
8
9
  CREATED:20240823T082829Z
9
10
  LAST-MODIFIED:20240823T082915Z
@@ -68,5 +68,11 @@ def test_special_timezones(run):
68
68
  def test_localtime_is_not_in_the_result_string(run):
69
69
  """We do not want 'localtime' to turn up as the result tz name."""
70
70
  result = run("first", "--tz", "localtime", "one-event-without-timezone.ics")
71
- assert f"DTSTART;TZID={get_localzone_name()}:201903" in result.output
72
- assert f"DTEND;TZID={get_localzone_name()}:201903" in result.output
71
+ assert (
72
+ f"DTSTART;TZID={get_localzone_name()}:201903" in result.output
73
+ or "TZID=" not in result.output
74
+ )
75
+ assert (
76
+ f"DTEND;TZID={get_localzone_name()}:201903" in result.output
77
+ or "TZID=" not in result.output
78
+ )
@@ -0,0 +1,68 @@
1
+ """We make sure that the result can be processed by another icalendar application.
2
+
3
+ See https://github.com/niccokunzmann/ics-query/issues/40
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ import pytest
11
+
12
+ from ics_query import __version__
13
+
14
+ if TYPE_CHECKING:
15
+ from icalendar import Calendar, Timezone
16
+
17
+ from ics_query.tests.conftest import ExampleRun
18
+
19
+
20
+ @pytest.fixture()
21
+ def calendar(run) -> ExampleRun:
22
+ """Return a calendar that is wrapped around the event."""
23
+ return run("first", "--as-calendar", "one-event-without-timezone.ics").calendar
24
+
25
+
26
+ def test_result_is_wrapped_in_a_calendar(calendar: Calendar):
27
+ """Add the calendar component around the event."""
28
+ assert calendar.name == "VCALENDAR"
29
+
30
+
31
+ def test_the_product_id_is_that_of_ics_query(calendar):
32
+ """The product id is set with version."""
33
+ assert calendar["PRODID"] == f"ics-query {__version__}"
34
+
35
+
36
+ def test_the_version_is_set(calendar):
37
+ """Version is required."""
38
+ assert calendar["VERSION"] == "2.0"
39
+
40
+
41
+ def test_no_timezone_is_included(calendar):
42
+ """We do not have timezones in this file, so there should be none."""
43
+ assert calendar.timezones == []
44
+
45
+
46
+ @pytest.mark.parametrize(
47
+ "file",
48
+ [
49
+ "one-event.ics", # one timezone
50
+ "multiple-calendars.ics", # same timezone twice
51
+ ],
52
+ )
53
+ def test_calendar_adds_timezones_automatically(run, file):
54
+ """Return a calendar that is wrapped around the event."""
55
+ calendar = run("first", "--as-calendar", file).calendar
56
+ assert len(calendar.timezones) == 1
57
+ tz: Timezone = calendar.timezones[0]
58
+ assert tz.tz_name == "Europe/Berlin"
59
+
60
+
61
+ def test_x_wr_timezone_is_added(run):
62
+ """X-WR-TIMEZONE requires adding the timezone component manually."""
63
+ calendar = run(
64
+ "first", "--as-calendar", "x-wr-timezone-rdate-hackerpublicradio.ics"
65
+ ).calendar
66
+ assert len(calendar.timezones) == 1
67
+ tz: Timezone = calendar.timezones[0]
68
+ assert tz.tz_name == "Europe/London"
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ics-query
3
- Version: 0.4.24
3
+ Version: 0.4.33
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/660bc730f47fb7ef98cf11571f2eb74053961149.zip
7
+ Project-URL: source_archive, https://github.com/niccokunzmann/ics-query/archive/c746775fade95f34ed6a84f7b7baefedfc1a04ff.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
@@ -697,11 +697,11 @@ Classifier: Programming Language :: Python :: 3.12
697
697
  Classifier: Topic :: Office/Business :: Scheduling
698
698
  Requires-Python: >=3.9
699
699
  Requires-Dist: click==8.1.8
700
- Requires-Dist: icalendar==6.1.1
700
+ Requires-Dist: icalendar==6.1.3
701
701
  Requires-Dist: python-dateutil==2.9.0.post0
702
- Requires-Dist: recurring-ical-events==3.5.2
702
+ Requires-Dist: recurring-ical-events==3.7.0
703
703
  Requires-Dist: six==1.17.0
704
- Requires-Dist: tzdata==2025.1
704
+ Requires-Dist: tzdata==2025.2
705
705
  Requires-Dist: tzlocal==5.3.1
706
706
  Requires-Dist: x-wr-timezone==2.0.1
707
707
  Provides-Extra: test
@@ -802,6 +802,8 @@ LAST-MODIFIED:20190303T111937
802
802
  END:VEVENT
803
803
  ```
804
804
 
805
+ #### Concatenating Calendars
806
+
805
807
  We can concatenate calendars and pipe them into `ics-query`.
806
808
  In the example below, we get all events that happen right now in two calendars.
807
809
 
@@ -819,6 +821,30 @@ You can pipe one or more calendars into the input.
819
821
  cat calendar.ics | ics-query first -
820
822
  ```
821
823
 
824
+ #### Valid ICS files
825
+
826
+ The resulting events are missing the timezone and the calendar information by default.
827
+ This information can be added using the `--as-calendar` parameter.
828
+ The result is a valid `.ics` file that can be processed further by other commands and programs.
829
+
830
+ In the example below, we use the calendar command to inspect the event for human readability.
831
+
832
+ ```shell
833
+ $ ics-query at --as-calendar 2014-05-03 x-wr-timezone-rdate-hackerpublicradio.ics event.ics
834
+ $ icalendar event.ics
835
+ Organizer:
836
+ Attendees:
837
+
838
+ Summary : HPR Community News
839
+ Starts : Sat May 3 20:00:00 2014
840
+ End : Sat May 3 22:00:00 2014
841
+ Duration : 2:00:00
842
+ Location : mumble.openspeak.cc port: 64747
843
+ Comment :
844
+ Description:
845
+ This is from http://www.hackerpublicradio.org/eps/hpr1286/iCalendar_Hacking_shownotes.html
846
+ ```
847
+
822
848
  ### Events at Certain Times
823
849
 
824
850
  You can query which events happen at certain times:
@@ -1035,6 +1061,13 @@ while `b` and `c` can change.
1035
1061
 
1036
1062
  This section should set you up for developing `ics-query`.
1037
1063
 
1064
+ To create new commits, you should install [pre-commit](https://pre-commit.com/).
1065
+ Then run:
1066
+
1067
+ ```shell
1068
+ pre-commit install
1069
+ ```
1070
+
1038
1071
  ### Testing
1039
1072
 
1040
1073
  This project's development is driven by tests.
@@ -1103,6 +1136,16 @@ We automatically release the versions that only update dependencies.
1103
1136
  If the version you installed does not show up here, only the dependencies
1104
1137
  have been updated.
1105
1138
 
1139
+ - v0.4.33
1140
+
1141
+ - Add `--as-calendar` parameter.
1142
+
1143
+ - v0.4.32
1144
+
1145
+ - Update dependencies.
1146
+ - Include recurrence ID in events to identify the occurrence in a series.
1147
+ - Update help message in command line.
1148
+
1106
1149
  - v0.4.1
1107
1150
 
1108
1151
  - Automatic release with patch level version number increased
@@ -1,31 +1,33 @@
1
1
  ics_query/__init__.py,sha256=BBd-FYDFf3jY_ApkMRkivqGti2uMeP4OT6KtgZDixic,917
2
2
  ics_query/__main__.py,sha256=1rFDbZZcd6llmQ-X_611jWdrua4HMOnpOclKRvf03Us,737
3
- ics_query/_version.py,sha256=eqPTx1mit5QVY_CIZPeIYTsnPPxmLZEDtDv2l58DdBE,513
4
- ics_query/cli.py,sha256=VPXcSCjtaWZJtSzMsHqLfcTgStnJsoyY9K8Q0VZUs3o,19993
3
+ ics_query/_version.py,sha256=mjzqdR2aEWjoUOVFIQeef0uCWnfAw-qUUoZHmfz-cBU,513
4
+ ics_query/cli.py,sha256=sXk2iQD2qCQ4kHfWmEjXzrtHfk2Jmrsi9FwXDNg_E68,22774
5
5
  ics_query/parse.py,sha256=AO2TBoe98exzfzTKsoF5ZAvJOg3hN2qRpP15DAQKZaM,2415
6
6
  ics_query/query.py,sha256=9HSSWJ1y7avU6mRDXBQKKTkk9oeG9qEwhr3k0piHbaU,2692
7
7
  ics_query/version.py,sha256=ByXlZC-vAOZfkpZfh05KiLVnOH8IcSrQzylVZmjcnpA,1518
8
8
  ics_query/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- ics_query/tests/conftest.py,sha256=MmxzXtpXHN3pf8jDmVGXvxcNXfepRwrJ03EF68zRSrY,3668
10
- ics_query/tests/test_command_line.py,sha256=mB_v-WqjpYRcxtROdi7GfvUGTEHYZ-3Z-sWevp7lFYc,2524
9
+ ics_query/tests/conftest.py,sha256=CwLgBoWOwLbHKIo38PjDSlJpyf_fdSdX06wLPEHnNKc,3985
10
+ ics_query/tests/test_command_line.py,sha256=33zCwyXIbgCrpGyozuibXD8VcDnvKHTV38YOKXdKCps,2636
11
+ ics_query/tests/test_issue_40_valid_calendar.py,sha256=tRC9s4MN_0uMuVwzkjpVWxfgw9ymhYL8jRv01wrQ-80,1976
11
12
  ics_query/tests/test_parse_date.py,sha256=EGD06AtO_kNDcYEY7vhI9rN45HiZPbRm2LkGZoVW6iw,2785
12
13
  ics_query/tests/test_parse_timedelta.py,sha256=EpM013rw-G61wI0hyWjtFOtE7xXo2DNsU3vSIULGZ1k,1469
13
- ics_query/tests/runs/all --tz Singapore one-event.ics -.run,sha256=sWF9T4kKzhnZYrgidUX5sgdJ5q95q1gp9vd8X65bIdk,218
14
- ics_query/tests/runs/all three-events.ics -.run,sha256=gdcQ-LKaM0Bpre06txuoTTk2WYIh4Qe2-v0weO2Ckkc,768
15
- ics_query/tests/runs/at 2019-03-04 multiple-calendars.ics -.run,sha256=Cyeg9DMK6BibiCwkM8nW0ZtULUYSZFIgDvKRlSo_jTw,482
16
- ics_query/tests/runs/at 2019-03-04 one-event-twice.ics -.run,sha256=hc-0yJhWpnSVzPNB_0_tJfR6dskNMm9xdbHeAhsX1zI,452
17
- ics_query/tests/runs/at 2019-03-04 one-event.ics -.run,sha256=GVVmVpqzFjQLw2_RuhxnVTpCnwflNqeWbkFZ6u5WXxA,226
18
- ics_query/tests/runs/at 2019-03-07 multiple-calendars.ics -.run,sha256=ZlLXxPMIai7vvmQTL4NyXJwdCQzoU1scEn_FpjmkkUA,256
19
- ics_query/tests/runs/at 2024-08-20 Berlin-Los-Angeles.ics -.run,sha256=KzQaFp65wntWfEu4fQwr3iH1kE-ooEmslA4UeA9-gkc,643
20
- ics_query/tests/runs/between 20240823 4d recurring-work-events.ics -.run,sha256=2_sM7CkSW4SDk6wbwyLURZ8WZm2rT0hgmx1k7O07T3M,574
21
- ics_query/tests/runs/first -c VJOURNAL -c VEVENT one-event.ics -.run,sha256=GVVmVpqzFjQLw2_RuhxnVTpCnwflNqeWbkFZ6u5WXxA,226
14
+ ics_query/tests/runs/all --tz Singapore one-event.ics -.run,sha256=0eX6yRqVK1b_ZEwrDy1AM3O69UHhpV35da6DjpGqBn4,263
15
+ ics_query/tests/runs/all three-events.ics -.run,sha256=p47kuGwMvx748zP5viAhWzRP7jPKw5DKC8MCejzIDgg,915
16
+ ics_query/tests/runs/at 2014-05-03 x-wr-timezone-rdate-hackerpublicradio.ics -.run,sha256=-XQkaf85KGgUOi4lmrD_cYfHs1J7BXQ231NGNIpE_xI,330
17
+ ics_query/tests/runs/at 2019-03-04 multiple-calendars.ics -.run,sha256=NSOHYgfKs6Cyz3P2VL3JXKctF1TH8m2BiphOAi9uJpY,580
18
+ ics_query/tests/runs/at 2019-03-04 one-event-twice.ics -.run,sha256=SLFIzMkRk-foIz1jJurQpoK7EeeLgFtqNPDqh76lp_Y,550
19
+ ics_query/tests/runs/at 2019-03-04 one-event.ics -.run,sha256=7-R6EbVWZJIUHfRk7SG1bxCtGMCuQ5DiIyP85LfeoQA,275
20
+ ics_query/tests/runs/at 2019-03-07 multiple-calendars.ics -.run,sha256=pDp8fcZKV2qAKPpEY5F7pJzHm0hhKmsRxP5GN9jGH-k,305
21
+ ics_query/tests/runs/at 2024-08-20 Berlin-Los-Angeles.ics -.run,sha256=FGyIqmrTQpRt4Q6xsEvsyP1VGdABZeU5q6uDrCYWp2U,747
22
+ ics_query/tests/runs/between 20240823 4d recurring-work-events.ics -.run,sha256=kgldttjMh1CPCys8qyf4Z87G8SOIq1MR0UC9-HYgg98,672
23
+ ics_query/tests/runs/first -c VJOURNAL -c VEVENT one-event.ics -.run,sha256=7-R6EbVWZJIUHfRk7SG1bxCtGMCuQ5DiIyP85LfeoQA,275
22
24
  ics_query/tests/runs/first -c VJOURNAL one-event.ics -.run,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- ics_query/tests/runs/first -c VJOURNAL simple-journal.ics -.run,sha256=Av_N2nsE2sfku6owvJwguQ53699C-GTHV5tWS8j5mZ4,370
24
- ics_query/tests/runs/first -c VTODO -c VJOURNAL simple-todo.ics -.run,sha256=-mcQWZIxMcIdSMOeHpdzAWDLXseGULNHOoGhy6ca8J0,224
25
- ics_query/tests/runs/first -c VTODO simple-todo.ics -.run,sha256=-mcQWZIxMcIdSMOeHpdzAWDLXseGULNHOoGhy6ca8J0,224
25
+ ics_query/tests/runs/first -c VJOURNAL simple-journal.ics -.run,sha256=h9hGts6XH8PndXC0AXc3rIJTDOGz_O6SI0IFMLgMFM8,404
26
+ ics_query/tests/runs/first -c VTODO -c VJOURNAL simple-todo.ics -.run,sha256=xDCKzWE34A62ipId3HpCEMX2DilMrIWN5GRKXNJiQ1U,255
27
+ ics_query/tests/runs/first -c VTODO simple-todo.ics -.run,sha256=xDCKzWE34A62ipId3HpCEMX2DilMrIWN5GRKXNJiQ1U,255
26
28
  ics_query/tests/runs/first empty-calendar.ics -.run,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
29
  ics_query/tests/runs/first empty-file.ics -.run,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- ics_query/tests/runs/first recurring-work-events.ics -.run,sha256=-mGNYgI4vBSkBMMJ0WCYdkg97JpnOJ64rlaqdSKCi2Y,287
30
+ ics_query/tests/runs/first recurring-work-events.ics -.run,sha256=8fa75wHxdVtnau1R39dnaDRZKlvclRd34P1Wr8i849U,336
29
31
  ics_query/tests/runs/calendars/Berlin-Los-Angeles.ics,sha256=_FFRUurDU_q7G8d8lSvfbCy3y1XvujaclLXei6GrW5U,9107
30
32
  ics_query/tests/runs/calendars/empty-calendar.ics,sha256=78162P3KYUj6Qhbdnjsm3E84jv--_--p5U0h_jovrpw,153
31
33
  ics_query/tests/runs/calendars/empty-file.ics,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -37,8 +39,9 @@ ics_query/tests/runs/calendars/recurring-work-events.ics,sha256=4ASS_-xZl2pVv1kZ
37
39
  ics_query/tests/runs/calendars/simple-journal.ics,sha256=U6_YMQM6EHwPDaszWgJDo-pR4WIqbJx8CRlHg4iu9zA,457
38
40
  ics_query/tests/runs/calendars/simple-todo.ics,sha256=l9uHQD4eyiCE8STvd2tj5lAVaBrqUtpF_M8Pv6yf1es,327
39
41
  ics_query/tests/runs/calendars/three-events.ics,sha256=YvPfthOzMCR0DQhGR6uj_1Bltgax3uA1ETlkI4JUI08,828
40
- ics_query-0.4.24.dist-info/METADATA,sha256=poJ8Zw712Q0pW7h8VGVj9mNrPgpch6kllwQ91a39_UI,54775
41
- ics_query-0.4.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
42
- ics_query-0.4.24.dist-info/entry_points.txt,sha256=Jq_39vCKVOkNZjL7Wngf_04V_n_QRszLgLT2CbJKiH4,49
43
- ics_query-0.4.24.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
44
- ics_query-0.4.24.dist-info/RECORD,,
42
+ ics_query/tests/runs/calendars/x-wr-timezone-rdate-hackerpublicradio.ics,sha256=QysB6tI9yfl4jDmaqEY2Jyv6ziu7YfKdfiK0zscd1Tw,839
43
+ ics_query-0.4.33.dist-info/METADATA,sha256=l0DF69BfK6Ih7yrhUpzt1ou599woxdHkbGiwnMtqQVE,55984
44
+ ics_query-0.4.33.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
+ ics_query-0.4.33.dist-info/entry_points.txt,sha256=Jq_39vCKVOkNZjL7Wngf_04V_n_QRszLgLT2CbJKiH4,49
46
+ ics_query-0.4.33.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
47
+ ics_query-0.4.33.dist-info/RECORD,,