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/parse.py CHANGED
@@ -1,13 +1,87 @@
1
+ # ics-query
2
+ # Copyright (C) 2024 Nicco Kunzmann
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
1
16
  """Functions for parsing the content."""
2
17
 
3
18
  from __future__ import annotations
4
19
 
5
- DateArgument = tuple[int]
20
+ import datetime
21
+ import re
22
+ from typing import Union
6
23
 
24
+ Date = tuple[int]
25
+ DateAndDelta = Union[Date, datetime.timedelta]
7
26
 
8
- def to_time(dt: str) -> DateArgument:
27
+
28
+ class InvalidTimeFormat(ValueError):
29
+ """The value provided does not yield a precise time."""
30
+
31
+
32
+ REGEX_TIME = re.compile(
33
+ r"^(?P<year>\d\d\d\d)"
34
+ r"(?P<month>-\d?\d|\d\d)?"
35
+ r"(?P<day>-\d?\d|\d\d)?"
36
+ r"(?P<hour>[ T]\d?\d|\d\d)?"
37
+ r"(?P<minute>:\d?\d|\d\d)?"
38
+ r"(?P<second>:\d?\d|\d\d)?"
39
+ r"$"
40
+ )
41
+
42
+ REGEX_TIMEDELTA = re.compile(
43
+ r"^\+?(?:(?P<days>\d+)d)?"
44
+ r"(?:(?P<hours>\d+)h)?"
45
+ r"(?:(?P<minutes>\d+)m)?"
46
+ r"(?:(?P<seconds>\d+)s)?"
47
+ r"$"
48
+ )
49
+
50
+
51
+ def to_time(dt: str) -> Date:
9
52
  """Parse the time and date."""
10
- return tuple(map(int, dt.split("-")))
53
+ parsed_dt = REGEX_TIME.match(dt)
54
+ if parsed_dt is None:
55
+ raise InvalidTimeFormat(dt)
56
+
57
+ def group(group_name: str) -> Date:
58
+ """Return a group's value."""
59
+ result = parsed_dt.group(group_name)
60
+ while result and result[0] not in "0123456789":
61
+ result = result[1:]
62
+ if result is None:
63
+ return ()
64
+ return (int(result),)
65
+
66
+ return (
67
+ group("year")
68
+ + group("month")
69
+ + group("day")
70
+ + group("hour")
71
+ + group("minute")
72
+ + group("second")
73
+ )
74
+
75
+
76
+ def to_time_and_delta(dt: str) -> DateAndDelta:
77
+ """Parse to a absolute time or timedelta."""
78
+ parsed_td = REGEX_TIMEDELTA.match(dt)
79
+ if parsed_td is None:
80
+ return to_time(dt)
81
+ kw = {k: int(v) for k, v in parsed_td.groupdict().items() if v is not None}
82
+ if not kw:
83
+ raise InvalidTimeFormat(dt)
84
+ return datetime.timedelta(**kw)
11
85
 
12
86
 
13
- __all__ = ["to_time", "DateArgument"]
87
+ __all__ = ["to_time", "Date", "to_time_and_delta", "DateAndDelta"]
ics_query/query.py ADDED
@@ -0,0 +1,71 @@
1
+ # ics-query
2
+ # Copyright (C) 2024 Nicco Kunzmann
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ """This is an adaptation of the CalendarQuery."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import datetime
21
+ import zoneinfo
22
+ from typing import TYPE_CHECKING
23
+
24
+ import x_wr_timezone
25
+ from recurring_ical_events import CalendarQuery, Occurrence
26
+
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Sequence
29
+
30
+ from icalendar import Calendar
31
+
32
+
33
+ class Query(CalendarQuery):
34
+ def __init__(self, calendar: Calendar, timezone: str, components: Sequence[str]):
35
+ """Create a new query."""
36
+ super().__init__(
37
+ x_wr_timezone.to_standard(calendar),
38
+ components=components,
39
+ skip_bad_series=True,
40
+ )
41
+ self.timezone = zoneinfo.ZoneInfo(timezone) if timezone else None
42
+
43
+ def with_timezone(self, dt: datetime.date | datetime.datetime):
44
+ """Add the timezone."""
45
+ if self.timezone is None:
46
+ return dt
47
+ if not isinstance(dt, datetime.datetime):
48
+ return datetime.datetime(
49
+ year=dt.year, month=dt.month, day=dt.day, tzinfo=self.timezone
50
+ )
51
+ if dt.tzinfo is None:
52
+ return dt.replace(tzinfo=self.timezone)
53
+ return dt.astimezone(self.timezone)
54
+
55
+ def _occurrences_between(
56
+ self,
57
+ start: datetime.date | datetime.datetime,
58
+ end: datetime.date | datetime.datetime,
59
+ ) -> list[Occurrence]:
60
+ """Override to adapt timezones."""
61
+ result = []
62
+ for occurrence in super()._occurrences_between(
63
+ self.with_timezone(start), self.with_timezone(end)
64
+ ):
65
+ occurrence.start = self.with_timezone(occurrence.start)
66
+ occurrence.end = self.with_timezone(occurrence.end)
67
+ result.append(occurrence)
68
+ return result
69
+
70
+
71
+ __all__ = ["Query"]
@@ -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
  """Configure the tests."""
2
17
 
3
18
  from __future__ import annotations
@@ -5,12 +20,13 @@ from __future__ import annotations
5
20
  import subprocess
6
21
  from copy import deepcopy
7
22
  from pathlib import Path
8
- from typing import NamedTuple
23
+ from typing import Callable, NamedTuple
9
24
 
10
25
  import pytest
11
26
 
12
27
  HERE = Path(__file__).parent
13
28
  IO_DIRECTORY = HERE / "runs"
29
+ CALENDARS_DIRECTORY = IO_DIRECTORY / "calendars"
14
30
 
15
31
 
16
32
  class TestRun(NamedTuple):
@@ -34,12 +50,26 @@ class TestRun(NamedTuple):
34
50
  )
35
51
 
36
52
 
53
+ def run_ics_query(*command, cwd=CALENDARS_DIRECTORY) -> TestRun:
54
+ """Run ics-qeury with a command."""
55
+ cmd = ["ics-query", *command]
56
+ print(" ".join(cmd))
57
+ completed_process = subprocess.run( # noqa: S603, RUF100
58
+ cmd, # noqa: S603, RUF100
59
+ capture_output=True,
60
+ timeout=3,
61
+ check=False,
62
+ cwd=cwd,
63
+ )
64
+ return TestRun.from_completed_process(completed_process)
65
+
66
+
37
67
  class IOTestCase(NamedTuple):
38
68
  """An example test case."""
39
69
 
40
70
  name: str
41
71
  command: list[str]
42
- cwd: Path
72
+ location: Path
43
73
  expected_output: str
44
74
 
45
75
  @classmethod
@@ -50,16 +80,7 @@ class IOTestCase(NamedTuple):
50
80
 
51
81
  def run(self) -> TestRun:
52
82
  """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)
83
+ return run_ics_query(*self.command)
63
84
 
64
85
 
65
86
  io_test_cases = [
@@ -75,4 +96,10 @@ def io_testcase(request) -> IOTestCase:
75
96
  return deepcopy(request.param)
76
97
 
77
98
 
99
+ @pytest.fixture
100
+ def run() -> Callable[..., TestRun]:
101
+ """Return a runner function."""
102
+ return run_ics_query
103
+
104
+
78
105
  __all__ = ["IOTestCase", "TestRun"]
@@ -0,0 +1,9 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:test1
3
+ DTSTART;TZID=Singapore:20190304T150000
4
+ DTEND;TZID=Singapore:20190304T153000
5
+ DTSTAMP:20190303T111937
6
+ UID:UYDQSG9TH4DE0WM3QFL2J
7
+ CREATED:20190303T111937
8
+ LAST-MODIFIED:20190303T111937
9
+ END:VEVENT
@@ -0,0 +1,33 @@
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:test4
14
+ DTSTART;TZID=Europe/Berlin:20190307T000000
15
+ DTEND;TZID=Europe/Berlin:20190307T010000
16
+ DTSTAMP:20190303T111937
17
+ UID:UYDQSG9TH4DE0WM3QFL2J
18
+ CLASS:PUBLIC
19
+ CREATED:20190303T111937
20
+ LAST-MODIFIED:20190303T111937
21
+ STATUS:CONFIRMED
22
+ END:VEVENT
23
+ BEGIN:VEVENT
24
+ SUMMARY:test4
25
+ DTSTART;TZID=Europe/Berlin:20190310T000000
26
+ DTEND;TZID=Europe/Berlin:20190310T010000
27
+ DTSTAMP:20190303T111937
28
+ UID:UYDQSG9TH4DE0WM3QFL2J
29
+ CLASS:PUBLIC
30
+ CREATED:20190303T111937
31
+ LAST-MODIFIED:20190303T111937
32
+ STATUS:CONFIRMED
33
+ END:VEVENT
@@ -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,23 @@
1
+ BEGIN:VEVENT
2
+ SUMMARY:6:00-7:00 Europe/Berlin 20th August
3
+ DTSTART;TZID=Europe/Berlin:20240820T060000
4
+ DTEND;TZID=Europe/Berlin:20240820T070000
5
+ DTSTAMP:20240823T130444Z
6
+ UID:b27ea261-f23d-4e03-a7ac-f8cb0d00f07f
7
+ CREATED:20240823T120639Z
8
+ LAST-MODIFIED:20240823T130444Z
9
+ TRANSP:OPAQUE
10
+ X-MOZ-GENERATION:3
11
+ END:VEVENT
12
+ BEGIN:VEVENT
13
+ SUMMARY:6:00-7:00 Amerika/Los Angeles 20th August
14
+ DTSTART;TZID=America/Los_Angeles:20240820T060000
15
+ DTEND;TZID=America/Los_Angeles:20240820T070000
16
+ DTSTAMP:20240823T130711Z
17
+ UID:6d7ff7f3-4bc4-4d89-afa0-771bd690518a
18
+ SEQUENCE:1
19
+ CREATED:20240823T120639Z
20
+ LAST-MODIFIED:20240823T130711Z
21
+ TRANSP:OPAQUE
22
+ X-MOZ-GENERATION:4
23
+ 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