diff --git a/syncall/caldav/caldav_side.py b/syncall/caldav/caldav_side.py index 53ed368..65871d9 100644 --- a/syncall/caldav/caldav_side.py +++ b/syncall/caldav/caldav_side.py @@ -14,7 +14,7 @@ from syncall.app_utils import error_and_exit from syncall.caldav.caldav_utils import calendar_todos, icalendar_component, map_ics_to_item -from syncall.sync_side import SyncSide +from syncall.sync_side import ItemType, SyncSide class CaldavSide(SyncSide): @@ -137,7 +137,7 @@ def set_(key: str, val: Any): # noqa: ANN401 todo.save() - def add_item(self, item): + def add_item(self, item) -> ItemType: todo = self._calendar.add_todo( summary=item.get("summary"), priority=item.get("priority"), diff --git a/syncall/taskwarrior/taskwarrior_side.py b/syncall/taskwarrior/taskwarrior_side.py index 1da0cd2..e84d8f9 100644 --- a/syncall/taskwarrior/taskwarrior_side.py +++ b/syncall/taskwarrior/taskwarrior_side.py @@ -259,6 +259,9 @@ def items_are_identical( item2: dict, ignore_keys: Sequence[str] = [], ) -> bool: + item1 = item1.copy() + item2 = item2.copy() + keys = [ k for k in [ diff --git a/syncall/tw_caldav_utils.py b/syncall/tw_caldav_utils.py index 8efcf3e..bd9da4b 100644 --- a/syncall/tw_caldav_utils.py +++ b/syncall/tw_caldav_utils.py @@ -9,7 +9,7 @@ from syncall.caldav.caldav_utils import parse_caldav_item_desc -CALDAV_TASK_CANCELLED_UDA = "caldav_completion_status" +CALDAV_TASK_CANCELLED_UDA = "syncall_caldav_task_cancelled" SYNCALL_TW_WAITING = "x-syncall-tw-waiting" SYNCALL_TW_UUID = "x-syncall-tw-uuid" @@ -91,6 +91,8 @@ def convert_tw_to_caldav(tw_item: Item) -> Item: # Priority if "priority" in tw_item: caldav_item["priority"] = aliases_tw_caldav_priority[tw_item["priority"].lower()] + else: + caldav_item["priority"] = "" # Timestamps if "entry" in tw_item: diff --git a/tests/test_data/sample_items.yaml b/tests/test_data/sample_items.yaml index a3f1e2d..b5f6829 100644 --- a/tests/test_data/sample_items.yaml +++ b/tests/test_data/sample_items.yaml @@ -34,26 +34,6 @@ tw_item_w_due: annotations: - This is the annotation text - This is the other annotation text -caldav_item_expected: - description: |- - This is the annotation text - This is the other annotation text - summary: Kalimera! - last-modified: 2019-03-05 00:03:09 - status: 'needs-action' - categories: ['remindme'] - x-syncall-tw-uuid: '00208973-20da-4988-ae3e-58ef3650c363' -caldav_item_w_date_expected: - description: |- - This is the annotation text - This is the other annotation text - due: 2019-03-09 00:00:00 - start: 2019-03-08 23:00:00 - summary: Kalimera! - last-modified: 2019-03-05 00:03:09 - status: 'needs-action' - categories: ['remindme'] - x-syncall-tw-uuid: '00208973-20da-4988-ae3e-58ef3650c363' gcal_item_expected: description: 'IMPORTED FROM TASKWARRIOR @@ -92,14 +72,6 @@ gcal_item_w_date: summary: gcal original event transparency: transparent updated: '2019-03-16T16:01:32.486Z' -caldav_item_w_date: - priority: '' - due: 2019-03-09 00:00:00 - id: 2s3acrn586e4ed7eff49ja91oh - start: 2019-03-10 23:00:00 - status: 'needs-action' - summary: 'gcal original event' - last-modified: 2019-03-16 16:01:32.486 tw_item_w_date_expected: syncallduration: "PT86400S" annotations: [] @@ -139,18 +111,6 @@ gcal_item: status: confirmed summary: another summary goes here updated: '2019-03-08T00:29:06.602Z' -caldav_item: - description: |- - kalimera - kalinuxta kai kali vradia - due: 2019-03-04 05:00:00 - id: 52ggba92l9ph4d0ghabi3ohdh7 - start: 2019-03-04 04:00:00 - status: 'needs-action' - summary: another summary goes here - last-modified: 2019-03-08 00:29:06.602 - priority: '' - x-syncall-tw-uuid: '00208973-20da-4988-ae3e-58ef3650c363' tw_item_expected: syncallduration: "PT3600S" annotations: diff --git a/tests/test_tw_caldav_conversions.py b/tests/test_tw_caldav_conversions.py index b38c2a5..c84e5ec 100644 --- a/tests/test_tw_caldav_conversions.py +++ b/tests/test_tw_caldav_conversions.py @@ -1,89 +1,235 @@ -from __future__ import annotations - -from typing import Any - -import yaml -from syncall.tw_caldav_utils import convert_caldav_to_tw, convert_tw_to_caldav - -from .generic_test_case import GenericTestCase - - -class TestConversions(GenericTestCase): - """Test item conversions - TW <-> Caldav Calendar.""" - - def load_sample_items(self): - with (GenericTestCase.DATA_FILES_PATH / "sample_items.yaml").open() as fname: - conts = yaml.load(fname, Loader=yaml.Loader) # noqa: S506 - - self.caldav_item = conts["caldav_item"] - self.tw_item_expected = conts["tw_item_expected"] - - self.tw_item: dict[str, Any] = conts["tw_item"] - self.tw_item_w_due = conts["tw_item_w_due"] - self.caldav_item_expected = conts["caldav_item_expected"] - self.caldav_item_w_date_expected = conts["caldav_item_w_date_expected"] - - self.caldav_item_w_date = conts["caldav_item_w_date"] - self.tw_item_w_date_expected = conts["tw_item_w_date_expected"] - - # we don't care about this field yet. - self.tw_item.pop("syncallduration") - self.tw_item_expected.pop("syncallduration") - - self.tw_item_w_date_expected.pop("syncallduration") - if "entry" in self.tw_item_w_date_expected: - self.tw_item_w_date_expected.pop("entry") - if "created" in self.tw_item_w_date_expected: - self.tw_item_w_date_expected.pop("created") - - def test_tw_caldav_basic_convert(self): - """Basic TW -> Caldav conversion.""" - self.load_sample_items() - caldav_item_out = convert_tw_to_caldav(self.tw_item) - caldav_item_out.pop("created", "") - assert caldav_item_out == self.caldav_item_expected - - def test_tw_caldav_w_due_convert(self): - """Basic TW (with 'due' subfield) -> Caldav conversion.""" - self.load_sample_items() - caldav_item_out = convert_tw_to_caldav(self.tw_item_w_due) - caldav_item_out.pop("created", "") - assert caldav_item_out == self.caldav_item_w_date_expected - - def test_caldav_tw_basic_convert(self): - """Basic Caldav -> TW conversion.""" - self.load_sample_items() - tw_item_out = convert_caldav_to_tw(self.caldav_item) - assert tw_item_out == self.tw_item_expected - - def test_caldav_tw_date_convert(self): - """Caldav (with 'date' subfield) -> TW conversion.""" - self.load_sample_items() - tw_item_out = convert_caldav_to_tw(self.caldav_item_w_date) - assert tw_item_out == self.tw_item_w_date_expected - - def test_tw_caldav_n_back(self): - """TW -> Caldav -> TW conversion.""" - self.load_sample_items() - - # UGLY - Rewrite how we do testing for caldav<>tw and gcal<>tw - intermediate_caldav = convert_tw_to_caldav(self.tw_item) - intermediate_caldav["priority"] = "" - intermediate_caldav.pop("created", "") - - tw_item_out = convert_caldav_to_tw(intermediate_caldav) - assert set(self.tw_item) ^ set(tw_item_out) == set({"id", "urgency", "entry"}) - - intersection = set(self.tw_item) & set(tw_item_out) - assert {i: self.tw_item[i] for i in intersection} == { - i: tw_item_out[i] for i in intersection - } - - def test_caldav_tw_n_back(self): - """Caldav -> TW -> Caldav conversion.""" - self.load_sample_items() - caldav_item_out = convert_tw_to_caldav(convert_caldav_to_tw(self.caldav_item)) - - # UGLY - Rewrite how we do testing for caldav<>tw and gcal<>tw - caldav_item_out["priority"] = "" - assert set(self.caldav_item) ^ set(caldav_item_out) == {"id"} +import datetime + +import pytest +from dateutil.tz import tzutc +from syncall.caldav.caldav_side import CaldavSide +from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide +from syncall.tw_caldav_utils import ( + CALDAV_TASK_CANCELLED_UDA, + SYNCALL_TW_UUID, + SYNCALL_TW_WAITING, + convert_caldav_to_tw, + convert_tw_to_caldav, +) + +tw_pending_item = { + "id": 5, + "description": "task in project - and with date", + "entry": datetime.datetime(2024, 8, 18, 7, 53, 40, tzinfo=tzutc()), + "modified": datetime.datetime(2024, 8, 18, 7, 53, 40, tzinfo=tzutc()), + "project": "kalimera", + "status": "pending", + "uuid": "4471f2ac-4a70-4012-9eff-b4ddfc860d26", + "urgency": 9.74399, +} + +caldav_pending_item = { + "summary": "task in project - and with date", + "description": "", + "priority": "", + SYNCALL_TW_UUID: "4471f2ac-4a70-4012-9eff-b4ddfc860d26", + "status": "needs-action", + SYNCALL_TW_WAITING: "false", + "created": datetime.datetime(2024, 8, 18, 7, 53, 40, tzinfo=tzutc()), + "last-modified": datetime.datetime(2024, 8, 18, 7, 53, 40, tzinfo=tzutc()), +} + +tw_pending_item_with_annotations = { + **tw_pending_item, + "annotations": ["annotation1", "annotation2"], +} + +caldav_pending_item_with_annotations = { + **caldav_pending_item, + "description": "annotation1\nannotation2", +} + +tw_waiting_item = { + **tw_pending_item, + "wait": datetime.datetime(2024, 8, 19, 8, 53, 40, tzinfo=tzutc()), + "status": "waiting", +} + +caldav_pending_item_waiting = { + **caldav_pending_item, + SYNCALL_TW_WAITING: "true", +} + +tw_pending_item_with_due = { + **tw_pending_item, + "due": datetime.datetime(2024, 8, 18, 10, 53, 40, tzinfo=tzutc()), +} + +caldav_item_with_due = { + **caldav_pending_item, + "start": datetime.datetime(2024, 8, 18, 9, 53, 40, tzinfo=tzutc()), + "due": datetime.datetime(2024, 8, 18, 10, 53, 40, tzinfo=tzutc()), +} + +tw_completed_item = { + **tw_pending_item, + "status": "completed", + "end": datetime.datetime(2024, 8, 18, 10, 53, 40, tzinfo=tzutc()), +} + +tw_completed_item2 = { + **tw_pending_item, + "status": "completed", + "end": datetime.datetime(2024, 8, 18, 10, 53, 40, tzinfo=tzutc()), + CALDAV_TASK_CANCELLED_UDA: "false", +} + +caldav_completed_item_base = { + **caldav_pending_item, + "completed": datetime.datetime(2024, 8, 18, 10, 53, 40, tzinfo=tzutc()), + SYNCALL_TW_WAITING: None, +} + +caldav_completed_item = { + **caldav_completed_item_base, + "status": "completed", +} + +tw_completed_item_with_cancelled_uda = { + **tw_completed_item, + "status": "completed", + "end": datetime.datetime(2024, 8, 18, 10, 53, 40, tzinfo=tzutc()), + CALDAV_TASK_CANCELLED_UDA: "true", +} + +caldav_cancelled_item = { + **caldav_completed_item_base, + "status": "cancelled", +} + + +def tw_pending_item_with_priority(prio: str) -> dict: + return { + **tw_pending_item, + "priority": prio, + } + + +def caldav_item_with_priority(prio: int) -> dict: + return { + **caldav_pending_item, + "priority": prio, + } + + +@pytest.mark.parametrize( + ("tw_item", "caldav_item_expected"), + [ + # pending_item + ( + tw_pending_item, + caldav_pending_item, + ), + # pending_item_with_annotations + ( + tw_pending_item_with_annotations, + caldav_pending_item_with_annotations, + ), + # pending_item_with_due + ( + tw_pending_item_with_due, + caldav_item_with_due, + ), + # completed_item + ( + tw_completed_item, + caldav_completed_item, + ), + # completed_item_with_cancelled_uda + ( + tw_completed_item2, + caldav_completed_item, + ), + # cancelled_item + ( + tw_completed_item_with_cancelled_uda, + caldav_cancelled_item, + ), + # pending_item_with_priority_{L,M,H} + *( + ( + tw_pending_item_with_priority(tw_prio), + caldav_item_with_priority(caldav_prio), + ) + for tw_prio, caldav_prio in [("L", 9), ("M", 5), ("H", 1)] + ), + ], + ids=[ + "pending_item", + "pending_item_with_annotations", + "pending_item_with_due", + "completed_item", + "completed_item_with_cancelled_uda", + "cancelled_item", + "pending_item_with_priority_L", + "pending_item_with_priority_M", + "pending_item_with_priority_H", + ], +) +def test_convert_tw_to_caldav_n_back(tw_item, caldav_item_expected): + caldav_item = convert_tw_to_caldav(tw_item) + assert CaldavSide.items_are_identical(caldav_item, caldav_item_expected) + + tw_item_reconverted = convert_caldav_to_tw(caldav_item) + assert TaskWarriorSide.items_are_identical(tw_item_reconverted, tw_item) + + +@pytest.mark.parametrize( + ("caldav_item", "tw_item_expected"), + [ + # pending_item + ( + caldav_pending_item, + tw_pending_item, + ), + # pending_item_with_annotations + ( + caldav_pending_item_with_annotations, + tw_pending_item_with_annotations, + ), + # pending_item_with_due + ( + caldav_item_with_due, + tw_pending_item_with_due, + ), + # completed_item + ( + caldav_completed_item, + tw_completed_item, + ), + # cancelled_item + ( + caldav_cancelled_item, + tw_completed_item_with_cancelled_uda, + ), + # pending_item_with_priority_{9,5,1} + *( + ( + caldav_item_with_priority(caldav_prio), + tw_pending_item_with_priority(tw_prio), + ) + for tw_prio, caldav_prio in [("L", 9), ("M", 5), ("H", 1)] + ), + ], + ids=[ + "pending_item", + "pending_item_with_annotations", + "pending_item_with_due", + "completed_item", + "cancelled_item", + "pending_item_with_priority_9", + "pending_item_with_priority_5", + "pending_item_with_priority_1", + ], +) +def test_convert_caldav_to_tw_n_back(caldav_item, tw_item_expected): + tw_item = convert_caldav_to_tw(caldav_item) + assert TaskWarriorSide.items_are_identical(tw_item, tw_item_expected) + + caldav_item_reconverted = convert_tw_to_caldav(tw_item) + assert CaldavSide.items_are_identical(caldav_item_reconverted, caldav_item)