Selaa lähdekoodia

add support for tasks (`VTODO`)

Fabian Peter Hammerle 4 päivää sitten
vanhempi
sitoutus
0a08626130
6 muutettua tiedostoa jossa 88 lisäystä ja 6 poistoa
  1. 2 1
      CHANGELOG.md
  2. 9 5
      ical2vdir/__init__.py
  3. 23 0
      tests/cli_test.py
  4. 17 0
      tests/event_test.py
  5. 25 0
      tests/resources/nextcloud-tasks.ics
  6. 12 0
      tests/vdir_test.py

+ 2 - 1
CHANGELOG.md

@@ -6,7 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 ### Added
-- support [icalendar](https://github.com/collective/icalendar/blob/main/CHANGES.rst)
+- support for tasks (`VTODO`)
+- support for [icalendar](https://github.com/collective/icalendar/blob/main/CHANGES.rst)
   library v5 & v6
 
 ### Fixed

+ 9 - 5
ical2vdir/__init__.py

@@ -55,7 +55,11 @@ def _event_prop_equal(prop_a: typing.Any, prop_b: typing.Any) -> bool:
     return typing.cast(bool, prop_a == prop_b and prop_a.params == prop_b.params)
 
 
-def _events_equal(event_a: icalendar.cal.Event, event_b: icalendar.cal.Event) -> bool:
+def _events_equal(
+    event_a: icalendar.cal.Component, event_b: icalendar.cal.Component
+) -> bool:
+    if event_a.name != event_b.name:  # "VEVENT", "VTODO"
+        return False
     for key, prop_a in event_a.items():
         if key == "DTSTAMP":
             continue
@@ -81,7 +85,7 @@ def _datetime_basic_isoformat(dt_obj: datetime.datetime) -> str:
     return dt_obj.strftime("%Y%m%dT%H%M%S%z")
 
 
-def _event_vdir_filename(event: icalendar.cal.Event) -> str:
+def _event_vdir_filename(event: icalendar.cal.Component) -> str:
     # > An item should contain a UID property as described by the vCard and iCalendar standards.
     # > [...] The filename should have similar properties as the UID of the file content.
     # > However, there is no requirement for these two to be the same.
@@ -98,7 +102,7 @@ def _event_vdir_filename(event: icalendar.cal.Event) -> str:
     return output_filename + _VDIR_EVENT_FILE_EXTENSION
 
 
-def _write_event(event: icalendar.cal.Event, path: pathlib.Path) -> None:
+def _write_event(event: icalendar.cal.Component, path: pathlib.Path) -> None:
     if path.is_dir():
         raise IsADirectoryError(path)  # similar to os.rename
     # > Creating and modifying items or metadata files should happen atomically.
@@ -119,7 +123,7 @@ def _write_event(event: icalendar.cal.Event, path: pathlib.Path) -> None:
 
 
 def _sync_event(
-    event: icalendar.cal.Event, output_dir_path: pathlib.Path
+    event: icalendar.cal.Component, output_dir_path: pathlib.Path
 ) -> pathlib.Path:
     output_path = output_dir_path.joinpath(_event_vdir_filename(event))
     if not output_path.exists():
@@ -189,7 +193,7 @@ def _main() -> None:
         if path.is_file() and path.name.endswith(_VDIR_EVENT_FILE_EXTENSION)
     )
     for component in calendar.subcomponents:
-        if isinstance(component, icalendar.cal.Event):
+        if isinstance(component, (icalendar.cal.Event, icalendar.cal.Todo)):
             extra_paths.discard(
                 _sync_event(event=component, output_dir_path=args.output_dir_path)
             )

+ 23 - 0
tests/cli_test.py

@@ -85,6 +85,29 @@ def test__main_create_all_recurrence_id_date(
     assert event["RECURRENCE-ID"].dt == datetime.date(2026, 2, 1)
 
 
+def test__main_create_all_tasks(
+    caplog: _pytest.logging.LogCaptureFixture, tmp_path: pathlib.Path
+) -> None:
+    with pathlib.Path(__file__).parent.joinpath(
+        "resources", "nextcloud-tasks.ics"
+    ).open("rb") as calendar_file:
+        with unittest.mock.patch("sys.stdin", calendar_file), unittest.mock.patch(
+            "sys.argv", ["", "--output-dir", str(tmp_path)]
+        ), caplog.at_level(logging.WARNING):
+            ical2vdir._main()
+    created_item_paths = sorted(tmp_path.iterdir())
+    assert [p.name for p in created_item_paths] == [
+        "1e6554b1-7ec6-4b58-9688-1dd141ea22cd.ics",
+        "d6c1f8fa-0dfa-4d31-a988-6e7876fe3222.ics",
+    ]
+    assert not caplog.records
+    task = icalendar.cal.Event.from_ical(created_item_paths[0].read_bytes())
+    assert isinstance(task, icalendar.cal.Todo)
+    assert task["UID"] == "1e6554b1-7ec6-4b58-9688-1dd141ea22cd"
+    assert task["SUMMARY"] == "test 2"
+    assert task["DUE"].dt == datetime.date(2026, 2, 9)
+
+
 def test__main_create_some(
     caplog: _pytest.logging.LogCaptureFixture,
     tmp_path: pathlib.Path,

+ 17 - 0
tests/event_test.py

@@ -187,6 +187,23 @@ UID:c27cfee4-60a5-4bc4-9ab6-ef4fb3dce111
 RRULE:FREQ=WEEKLY;WKST=TU
 EXDATE;TZID=Europe/Vienna:20260120T160000
 END:VEVENT
+""",
+                False,
+            ),
+            (
+                b"""BEGIN:VEVENT
+UID:d6c1f8fa-0dfa-4d31-a988-6e7876fe3222
+DTSTAMP:20260208T070338Z
+SUMMARY:test
+DTSTART;VALUE=DATE:20260208
+END:VEVENT
+""",
+                b"""BEGIN:VTODO
+UID:d6c1f8fa-0dfa-4d31-a988-6e7876fe3222
+DTSTAMP:20260208T070338Z
+SUMMARY:test
+DTSTART;VALUE=DATE:20260208
+END:VTODO
 """,
                 False,
             ),

+ 25 - 0
tests/resources/nextcloud-tasks.ics

@@ -0,0 +1,25 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//SabreDAV//SabreDAV//EN
+X-WR-CALNAME:some tasks
+X-APPLE-CALENDAR-COLOR:#ff0000
+REFRESH-INTERVAL;VALUE=DURATION:PT4H
+X-PUBLISHED-TTL:PT4H
+BEGIN:VTODO
+UID:1e6554b1-7ec6-4b58-9688-1dd141ea22cd
+CREATED:20260208T070332Z
+LAST-MODIFIED:20260208T070343Z
+DTSTAMP:20260208T070343Z
+SUMMARY:test 2
+DUE;VALUE=DATE:20260209
+END:VTODO
+BEGIN:VTODO
+UID:d6c1f8fa-0dfa-4d31-a988-6e7876fe3222
+CREATED:20260208T070025Z
+LAST-MODIFIED:20260208T070338Z
+DTSTAMP:20260208T070338Z
+SUMMARY:test 1
+DTSTART;VALUE=DATE:20260208
+END:VTODO
+END:VCALENDAR

+ 12 - 0
tests/vdir_test.py

@@ -99,6 +99,18 @@ END:VEVENT
 """,
             "1qa2ws3ed4rf5tg@google.com.20150924T090000+0200.ics",
         ),
+        (
+            b"""BEGIN:VTODO
+UID:d6c1f8fa-0dfa-4d31-a988-6e7876fe3222
+CREATED:20260208T070025Z
+LAST-MODIFIED:20260208T070338Z
+DTSTAMP:20260208T070338Z
+SUMMARY:test 1
+DTSTART;VALUE=DATE:20260208
+END:VTODO
+""",
+            "d6c1f8fa-0dfa-4d31-a988-6e7876fe3222.ics",
+        ),
     ],
 )
 def test__event_vdir_filename(event_ical: bytes, expected_filename: str) -> None: