ics-query 0.1.dev8__py3-none-any.whl → 0.2.0a0__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/_version.py CHANGED
@@ -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.dev8'
16
- __version_tuple__ = version_tuple = (0, 1, 'dev8')
15
+ __version__ = version = '0.2.0a0'
16
+ __version_tuple__ = version_tuple = (0, 2, 0)
ics_query/cli.py CHANGED
@@ -9,17 +9,17 @@ import typing as t
9
9
 
10
10
  import click
11
11
  import recurring_ical_events
12
- from icalendar import Calendar
13
- from recurring_ical_events import CalendarQuery
12
+ from icalendar.cal import Calendar, Component
14
13
 
15
14
  from . import parse
15
+ from .version import __version__
16
16
 
17
17
  if t.TYPE_CHECKING:
18
18
  from io import FileIO
19
19
 
20
20
  from icalendar.cal import Component
21
21
 
22
- from .parse import DateArgument
22
+ from .parse import Date
23
23
 
24
24
  print = functools.partial(print, file=sys.stderr) # noqa: A001
25
25
 
@@ -35,6 +35,11 @@ class ComponentsResult:
35
35
  """Return a component."""
36
36
  self._file.write(component.to_ical())
37
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
+
38
43
 
39
44
  class ComponentsResultArgument(click.File):
40
45
  """Argument for the result."""
@@ -50,6 +55,30 @@ class ComponentsResultArgument(click.File):
50
55
  return ComponentsResult(file)
51
56
 
52
57
 
58
+ class JoinedCalendars:
59
+ def __init__(self, calendars: list[Calendar]):
60
+ """Join multiple calendars."""
61
+ self.queries = [recurring_ical_events.of(calendar) for calendar in calendars]
62
+
63
+ def at(self, dt: tuple[int]) -> t.Generator[Component]:
64
+ """Return the components."""
65
+ for query in self.queries:
66
+ yield from query.at(dt)
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
+
81
+
53
82
  class CalendarQueryInputArgument(click.File):
54
83
  """Argument for the result."""
55
84
 
@@ -61,8 +90,8 @@ class CalendarQueryInputArgument(click.File):
61
90
  ) -> recurring_ical_events.CalendarQuery:
62
91
  """Return a CalendarQuery."""
63
92
  file = super().convert(value, param, ctx)
64
- calendar = Calendar.from_ical(file.read())
65
- return recurring_ical_events.of(calendar)
93
+ calendars = Calendar.from_ical(file.read(), multiple=True)
94
+ return JoinedCalendars(calendars)
66
95
 
67
96
 
68
97
  arg_calendar = click.argument("calendar", type=CalendarQueryInputArgument("rb"))
@@ -70,9 +99,48 @@ arg_output = click.argument("output", type=ComponentsResultArgument("wb"))
70
99
 
71
100
 
72
101
  @click.group()
102
+ @click.version_option(__version__)
73
103
  def main():
74
- """Simple program that greets NAME for a total of COUNT times."""
75
- # sys.stdout = sys.stderr # remove accidential print impact
104
+ """Find out what happens in ICS calendar files.
105
+
106
+ ics-query can query and filter RFC 5545 compatible .ics files.
107
+ Components are events, journal entries and TODOs.
108
+
109
+ \b
110
+ Common Parameters
111
+ -----------------
112
+
113
+ Common parameters are described below.
114
+
115
+ CALENDAR
116
+
117
+ The CALENDAR is a readable file with one or more ICS calendars in it.
118
+ If CALENDAR is "-", then the standard input is used.
119
+
120
+ OUTPUT
121
+
122
+ This is the OUTPUT file for the result.
123
+ It is usually a path to a file that can be written to.
124
+ If OUTPUT is "-", then the standard output is used.
125
+
126
+ \b
127
+ Notes on Calculation
128
+ --------------------
129
+
130
+ An event can be very long. If you request smaller time spans or a time as
131
+ exact as a second, the event will still occur within this time span if it
132
+ happens during that time.
133
+
134
+ Generally, an event occurs within a time span if this applies:
135
+
136
+ event.DTSTART <= span.DTEND and span.DTSTART < event.DTEND
137
+
138
+ The START is INCLUSIVE, then END is EXCLUSIVE.
139
+
140
+ \b
141
+ Notes on Timezones
142
+ ------------------
143
+ """ # noqa: D301
76
144
 
77
145
 
78
146
  pass_datetime = click.make_pass_decorator(parse.to_time)
@@ -82,11 +150,326 @@ pass_datetime = click.make_pass_decorator(parse.to_time)
82
150
  @click.argument("date", type=parse.to_time)
83
151
  @arg_calendar
84
152
  @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)
153
+ def at(calendar: JoinedCalendars, output: ComponentsResult, date: Date):
154
+ """Occurrences at a certain dates.
155
+
156
+ YEAR
157
+
158
+ All occurrences in this year.
159
+
160
+ \b
161
+ Formats:
162
+ \b
163
+ YYYY
164
+ \b
165
+ Examples:
166
+ \b
167
+ ics-query at 2024 # all occurrences in year 2024
168
+ ics-query at `date +%Y` # all occurrences in this year
169
+
170
+ MONTH
171
+
172
+ All occurrences in this month.
173
+
174
+ \b
175
+ Formats:
176
+ \b
177
+ YYYY-MM
178
+ YYYY-M
179
+ YYYYMM
180
+ \b
181
+ Examples:
182
+ \b
183
+ ics-query at 2019-10 # October 2019
184
+ ics-query at 1990-01 # January 1990
185
+ ics-query at 1990-1 # January 1990
186
+ ics-query at 199001 # January 1990
187
+ ics-query at `date +%Y%m` # this month
188
+
189
+ DAY
190
+
191
+ All occurrences in one day.
192
+
193
+ \b
194
+ Formats:
195
+ \b
196
+ YYYY-MM-DD
197
+ YYYY-M-D
198
+ YYYYMMDD
199
+ \b
200
+ Examples:
201
+ \b
202
+ ics-query at 1990-01-01 # 1st January 1990
203
+ ics-query at 1990-1-1 # 1st January 1990
204
+ ics-query at 19900101 # 1st January 1990
205
+ ics-query at `date +%Y%m%d` # today
206
+
207
+ HOUR
208
+
209
+ All occurrences within one hour.
210
+
211
+ \b
212
+ Formats:
213
+ \b
214
+ YYYY-MM-DD HH
215
+ YYYY-MM-DDTHH
216
+ YYYY-M-DTH
217
+ YYYYMMDDTHH
218
+ YYYYMMDDHH
219
+ \b
220
+ Examples:
221
+ \b
222
+ ics-query at 1990-01-01 00 # 1st January 1990, 12am - 1am
223
+ ics-query at 1990-01-01T00 # 1st January 1990, 12am - 1am
224
+ ics-query at 1990-1-1T17 # 1st January 1990, 17:00 - 18:00
225
+ ics-query at 19900101T23 # 1st January 1990, 23:00 - midnight
226
+ ics-query at 1990010123 # 1st January 1990, 23:00 - midnight
227
+ ics-query at `date +%Y%m%d%H` # this hour
228
+
229
+ MINUTE
230
+
231
+ All occurrences within one minute.
232
+
233
+ \b
234
+ Formats:
235
+ \b
236
+ YYYY-MM-DD HH:MM
237
+ YYYY-MM-DDTHH:MM
238
+ YYYY-M-DTH:M
239
+ YYYYMMDDTHHMM
240
+ YYYYMMDDHHMM
241
+ \b
242
+ Examples:
243
+ \b
244
+ ics-query at 1990-01-01 10:10 # 1st January 1990, 10:10am - 10:11am
245
+ ics-query at 1990-01-01T10:10 # 1st January 1990, 10:10am - 10:11am
246
+ ics-query at 1990-1-1T7:2 # 1st January 1990, 07:02 - 07:03
247
+ ics-query at 19900101T2359 # 1st January 1990, 23:59 - midnight
248
+ ics-query at 199001012359 # 1st January 1990, 23:59 - midnight
249
+ ics-query at `date +%Y%m%d%H%M` # this minute
250
+
251
+ SECOND
252
+
253
+ All occurrences at a precise time.
254
+
255
+ \b
256
+ Formats:
257
+ \b
258
+ YYYY-MM-DD HH:MM:SS
259
+ YYYY-MM-DDTHH:MM:SS
260
+ YYYY-M-DTH:M:S
261
+ YYYYMMDDTHHMMSS
262
+ YYYYMMDDHHMMSS
263
+ \b
264
+ Examples:
265
+ \b
266
+ ics-query at 1990-01-01 10:10:00 # 1st January 1990, 10:10am
267
+ ics-query at 1990-01-01T10:10:00 # 1st January 1990, 10:10am
268
+ ics-query at 1990-1-1T7:2:30 # 1st January 1990, 07:02:30
269
+ ics-query at 19901231T235959 # 31st December 1990, 23:59:59
270
+ ics-query at 19900101235959 # 1st January 1990, 23:59:59
271
+ ics-query at `date +%Y%m%d%H%M%S` # now
272
+ """ # noqa: D301
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))
90
473
 
91
474
 
92
475
  __all__ = ["main"]
ics_query/parse.py CHANGED
@@ -2,12 +2,71 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- DateArgument = tuple[int]
5
+ import datetime
6
+ import re
7
+ from typing import Union
6
8
 
9
+ Date = tuple[int]
10
+ DateAndDelta = Union[Date, datetime.timedelta]
7
11
 
8
- def to_time(dt: str) -> DateArgument:
12
+
13
+ class InvalidTimeFormat(ValueError):
14
+ """The value provided does not yield a precise time."""
15
+
16
+
17
+ REGEX_TIME = re.compile(
18
+ r"^(?P<year>\d\d\d\d)"
19
+ r"(?P<month>-\d?\d|\d\d)?"
20
+ r"(?P<day>-\d?\d|\d\d)?"
21
+ r"(?P<hour>[ T]\d?\d|\d\d)?"
22
+ r"(?P<minute>:\d?\d|\d\d)?"
23
+ r"(?P<second>:\d?\d|\d\d)?"
24
+ r"$"
25
+ )
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
+
35
+
36
+ def to_time(dt: str) -> Date:
9
37
  """Parse the time and date."""
10
- return tuple(map(int, dt.split("-")))
38
+ parsed_dt = REGEX_TIME.match(dt)
39
+ if parsed_dt is None:
40
+ raise InvalidTimeFormat(dt)
41
+
42
+ def group(group_name: str) -> Date:
43
+ """Return a group's value."""
44
+ result = parsed_dt.group(group_name)
45
+ while result and result[0] not in "0123456789":
46
+ result = result[1:]
47
+ if result is None:
48
+ return ()
49
+ return (int(result),)
50
+
51
+ return (
52
+ group("year")
53
+ + group("month")
54
+ + group("day")
55
+ + group("hour")
56
+ + group("minute")
57
+ + group("second")
58
+ )
59
+
60
+
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)
11
70
 
12
71
 
13
- __all__ = ["to_time", "DateArgument"]
72
+ __all__ = ["to_time", "Date", "to_time_and_delta", "DateAndDelta"]
@@ -5,12 +5,13 @@ from __future__ import annotations
5
5
  import subprocess
6
6
  from copy import deepcopy
7
7
  from pathlib import Path
8
- from typing import NamedTuple
8
+ from typing import Callable, NamedTuple
9
9
 
10
10
  import pytest
11
11
 
12
12
  HERE = Path(__file__).parent
13
13
  IO_DIRECTORY = HERE / "runs"
14
+ CALENDARS_DIRECTORY = IO_DIRECTORY / "calendars"
14
15
 
15
16
 
16
17
  class TestRun(NamedTuple):
@@ -34,12 +35,26 @@ class TestRun(NamedTuple):
34
35
  )
35
36
 
36
37
 
38
+ def run_ics_query(*command, cwd=CALENDARS_DIRECTORY) -> TestRun:
39
+ """Run ics-qeury with a command."""
40
+ cmd = ["ics-query", *command]
41
+ print(" ".join(cmd))
42
+ completed_process = subprocess.run( # noqa: S603, RUF100
43
+ cmd, # noqa: S603, RUF100
44
+ capture_output=True,
45
+ timeout=3,
46
+ check=False,
47
+ cwd=cwd,
48
+ )
49
+ return TestRun.from_completed_process(completed_process)
50
+
51
+
37
52
  class IOTestCase(NamedTuple):
38
53
  """An example test case."""
39
54
 
40
55
  name: str
41
56
  command: list[str]
42
- cwd: Path
57
+ location: Path
43
58
  expected_output: str
44
59
 
45
60
  @classmethod
@@ -50,16 +65,7 @@ class IOTestCase(NamedTuple):
50
65
 
51
66
  def run(self) -> TestRun:
52
67
  """Run this test case and return the result."""
53
- command = ["ics-query", *self.command]
54
- print(" ".join(command))
55
- completed_process = subprocess.run( # noqa: S603, RUF100
56
- command, # noqa: S603, RUF100
57
- capture_output=True,
58
- timeout=3,
59
- check=False,
60
- cwd=self.cwd / "calendars",
61
- )
62
- return TestRun.from_completed_process(completed_process)
68
+ return run_ics_query(*self.command)
63
69
 
64
70
 
65
71
  io_test_cases = [
@@ -75,4 +81,10 @@ def io_testcase(request) -> IOTestCase:
75
81
  return deepcopy(request.param)
76
82
 
77
83
 
84
+ @pytest.fixture
85
+ def run() -> Callable[..., TestRun]:
86
+ """Return a runner function."""
87
+ return run_ics_query
88
+
89
+
78
90
  __all__ = ["IOTestCase", "TestRun"]
@@ -0,0 +1,20 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:test4
3
+ DTSTART;TZID=Europe/Berlin:20190304T000000
4
+ DTEND;TZID=Europe/Berlin:20190304T010000
5
+ DTSTAMP:20190303T111937
6
+ UID:UYDQSG9TH4DE0WM3QFL2J
7
+ CLASS:PUBLIC
8
+ CREATED:20190303T111937
9
+ LAST-MODIFIED:20190303T111937
10
+ STATUS:CONFIRMED
11
+ END:VEVENT
12
+ BEGIN:VEVENT
13
+ SUMMARY:test1
14
+ DTSTART;TZID=Europe/Berlin:20190304T080000
15
+ DTEND;TZID=Europe/Berlin:20190304T083000
16
+ DTSTAMP:20190303T111937
17
+ UID:UYDQSG9TH4DE0WM3QFL2J
18
+ CREATED:20190303T111937
19
+ LAST-MODIFIED:20190303T111937
20
+ END:VEVENT
@@ -0,0 +1,18 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:test1
3
+ DTSTART;TZID=Europe/Berlin:20190304T080000
4
+ DTEND;TZID=Europe/Berlin:20190304T083000
5
+ DTSTAMP:20190303T111937
6
+ UID:UYDQSG9TH4DE0WM3QFL2J
7
+ CREATED:20190303T111937
8
+ LAST-MODIFIED:20190303T111937
9
+ END:VEVENT
10
+ BEGIN:VEVENT
11
+ SUMMARY:test1
12
+ DTSTART;TZID=Europe/Berlin:20190304T080000
13
+ DTEND;TZID=Europe/Berlin:20190304T083000
14
+ DTSTAMP:20190303T111937
15
+ UID:UYDQSG9TH4DE0WM3QFL2J
16
+ CREATED:20190303T111937
17
+ LAST-MODIFIED:20190303T111937
18
+ END:VEVENT
@@ -0,0 +1,11 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:test4
3
+ DTSTART;TZID=Europe/Berlin:20190307T000000
4
+ DTEND;TZID=Europe/Berlin:20190307T010000
5
+ DTSTAMP:20190303T111937
6
+ UID:UYDQSG9TH4DE0WM3QFL2J
7
+ CLASS:PUBLIC
8
+ CREATED:20190303T111937
9
+ LAST-MODIFIED:20190303T111937
10
+ STATUS:CONFIRMED
11
+ END:VEVENT
@@ -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
File without changes