From 428bdcb8686a19cb514ca8d80373d405e34787ac Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 18:35:37 +0200 Subject: [PATCH 01/22] fixed first part of robur-problems - caught the second part in a unit test --- caldav/calendarobjectresource.py | 9 ++++++++- caldav/collection.py | 4 +++- caldav/compatibility_hints.py | 24 +++++++++++++++++++++--- caldav/davclient.py | 1 + tests/conf.py | 10 ++++++++-- tests/test_caldav.py | 2 ++ tests/test_caldav_unit.py | 16 ++++++++++++++++ 7 files changed, 59 insertions(+), 7 deletions(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index 0205576f..a3df2370 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -61,7 +61,9 @@ from typing import Self from .davobject import DAVObject -from .elements import cdav, dav +from .elements.cdav import CalendarData +from .elements import cdav +from .elements import dav from .lib import error from .lib import vcal from .lib.error import errmsg @@ -629,7 +631,12 @@ def load_by_multiget(self) -> Self: response = self.parent._query(root, 1, "report") results = response.expand_simple_props([CalendarData()]) error.assert_(len(results) == 1) + if not href in results: + href2 = href + href = next(iter(results.keys())) + error.weirdness(f"{href} != {href2}") data = results[href][CalendarData.tag] + breakpoint() error.assert_(data) self.data = data return self diff --git a/caldav/collection.py b/caldav/collection.py index 8d0a7fad..3e642058 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -59,7 +59,9 @@ from .calendarobjectresource import Journal from .calendarobjectresource import Todo from .davobject import DAVObject -from .elements import cdav, dav +from .elements.cdav import CalendarData +from .elements import cdav +from .elements import dav from .lib import error from .lib import vcal from .lib.python_utilities import to_wire diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index 9b427b6c..7df73aee 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -268,6 +268,9 @@ 'no_search': """Apparently the calendar server does not support search at all (this often implies that 'object_by_uid_is_broken' has to be set as well)""", + 'no_search_openended': + """An open-ended search will not work""", + 'no_events_and_tasks_on_same_calendar': """Zimbra has the concept of task lists ... a calendar must either be a calendar with only events, or it can be a task list, but those must never be mixed""" } @@ -419,10 +422,11 @@ 'fragile_sync_tokens', ## no issue raised yet 'vtodo_datesearch_nodtstart_task_is_skipped', ## no issue raised yet 'broken_expand_on_exceptions', ## no issue raised yet - 'date_todo_search_ignores_duration' + 'date_todo_search_ignores_duration', 'calendar_color', 'calendar_order', - 'vtodo_datesearch_notime_task_is_skipped' + 'vtodo_datesearch_notime_task_is_skipped', + "no_alarmsearch", ] google = [ @@ -448,6 +452,9 @@ nextcloud = [ 'date_search_ignores_duration', + 'unique_calendar_ids', + 'broken_expand', + 'no_delete_calendar', 'sync_breaks_on_delete', 'no_recurring_todo', 'combined_search_not_working', @@ -488,7 +495,7 @@ 'text_search_not_working', 'no_relships', 'isnotdefined_not_working', - 'robur_rrule_freq_yearly_expands_monthly' + 'no_alarmsearch', ] posteo = [ @@ -530,4 +537,15 @@ 'search_delay' ] +gmx = [ + "no_scheduling_mailbox", + "no_mkcalendar", + "search_needs_comptype", + "text_search_is_case_insensitive", + "no_freebusy_rfc4791", + "no_expand", + "no_search_openended", + #"broken_expand_on_exceptions", ## should be implied by no_expand? +] + # fmt: on diff --git a/caldav/davclient.py b/caldav/davclient.py index 0f5d60b2..b5a961a7 100644 --- a/caldav/davclient.py +++ b/caldav/davclient.py @@ -897,6 +897,7 @@ def auto_conn( try: idx = int(name) + name = None except ValueError: idx = None try: diff --git a/tests/conf.py b/tests/conf.py index bcfba3ae..f810e49f 100644 --- a/tests/conf.py +++ b/tests/conf.py @@ -33,7 +33,13 @@ try: from .conf_private import caldav_servers except ImportError: - caldav_servers = [] + try: + from conf_private import caldav_servers + except ImportError: + try: + from tests.conf_private import caldav_servers + except ImportError: + caldav_servers = [] try: from .conf_private import test_private_test_servers @@ -241,7 +247,7 @@ def client( ): kwargs_ = kwargs.copy() no_args = not any(x for x in kwargs if kwargs[x] is not None) - if idx is None and no_args and caldav_servers: + if idx is None and name is None and no_args and caldav_servers: ## No parameters given - find the first server in caldav_servers list return client(idx=0) elif idx is not None and no_args and caldav_servers: diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 76054913..4ae138e6 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -2550,6 +2550,8 @@ def testCreateOverwriteDeleteEvent(self): # Create calendar c = self._fixCalendar() assert c.url is not None + + import pdb; pdb.set_trace() # attempts on updating/overwriting a non-existing event should fail (unless object_by_uid_is_broken): if not self.check_compatibility_flag("object_by_uid_is_broken"): diff --git a/tests/test_caldav_unit.py b/tests/test_caldav_unit.py index c45d682c..3c9d60ad 100755 --- a/tests/test_caldav_unit.py +++ b/tests/test_caldav_unit.py @@ -272,6 +272,22 @@ def testRequestNonAscii(self, mocked): assert response.status == 200 assert response.tree is None + def testLoadByMultiGet404(self): + xml = """ + + + /calendars/pythoncaldav-test/20010712T182145Z-123401%40example.com.ics + HTTP/1.1 404 Not Found + +""" + client = MockedDAVClient(xml) + calendar = Calendar( + client, url="/calendar/issue491/" + ) + object = Event(url="/calendar/issue491/notfound.ics", parent=calendar) + with pytest.raises(error.NotFoundError): + object.load_by_multiget() + @mock.patch("caldav.davclient.niquests.Session.request") def testRequestCustomHeaders(self, mocked): """ From e73c22d5a92847586803e82aa20ae4b69cd2c0d5 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sat, 17 May 2025 19:06:24 +0200 Subject: [PATCH 02/22] redusing the extreme rate limiting breaks in test --- tests/test_caldav.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 4ae138e6..01f1041e 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -554,7 +554,7 @@ def testInviteAndRespond(self): ## inbox/outbox? -def _delay_decorator(f, t=60): +def _delay_decorator(f, t=10): def foo(*a, **kwa): time.sleep(t) return f(*a, **kwa) From d37afb41675588e4d460e18f0e06e636ad1590f2 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 21:18:04 +0200 Subject: [PATCH 03/22] foo --- caldav/calendarobjectresource.py | 4 ++-- caldav/collection.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index a3df2370..f280aaa9 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -608,10 +608,10 @@ def load(self, only_if_unloaded: bool = False) -> Self: try: r = self.client.request(str(self.url)) + if r.status == 404: + raise error.NotFoundError(errmsg(r)) except: return self.load_by_multiget() - if r.status == 404: - raise error.NotFoundError(errmsg(r)) self.data = vcal.fix(r.raw) if "Etag" in r.headers: self.props[dav.GetEtag.tag] = r.headers["Etag"] diff --git a/caldav/collection.py b/caldav/collection.py index 3e642058..ee807d25 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -583,7 +583,7 @@ def calendar_multiget(self, event_urls: Iterable[URL]) -> List[_CC]: response = self._query(root, 1, "report") results = response.expand_simple_props([cdav.CalendarData()]) rv = [ - Event( + CalendarObjectResource( self.client, url=self.url.join(r), data=results[r][cdav.CalendarData.tag], From ba33f9a3d32f507d736778eeeb99fc434aaa725d Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 22:06:26 +0200 Subject: [PATCH 04/22] fix --- caldav/calendarobjectresource.py | 18 ++++------------- caldav/collection.py | 34 +++++++++++++++++++++----------- caldav/davclient.py | 4 +++- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index f280aaa9..eacee67c 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -625,20 +625,10 @@ def load_by_multiget(self) -> Self: with a multiget query """ error.assert_(self.url) - href = self.url.path - prop = dav.Prop() + CalendarData() - root = cdav.CalendarMultiGet() + prop + dav.Href(value=href) - response = self.parent._query(root, 1, "report") - results = response.expand_simple_props([CalendarData()]) - error.assert_(len(results) == 1) - if not href in results: - href2 = href - href = next(iter(results.keys())) - error.weirdness(f"{href} != {href2}") - data = results[href][CalendarData.tag] - breakpoint() - error.assert_(data) - self.data = data + mydata = self.parent._multiget(event_urls=[self.url], raise_notfound=True) + self.data = next(mydata) + assert_(self.data) + assert_(next(mydata, None) is None) return self ## TODO: self.id should either always be available or never diff --git a/caldav/collection.py b/caldav/collection.py index ee807d25..c7c50d3b 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -560,15 +560,12 @@ def save(self): self._create(id=self.id, name=self.name, **self.extra_init_options) return self - ## TODO: this is missing test code. - ## TODO: needs refactoring: - ## Objects found may be Todo and Journal, not only Event. - ## Replace the last lines with _request_report_build_resultlist method - def calendar_multiget(self, event_urls: Iterable[URL]) -> List[_CC]: + #def data2object_class + + def _multiget(self, event_urls: Iterable[URL], raise_notfound=False) -> Iterable[str]: """ get multiple events' data @author mtorange@gmail.com - @type events list of Event """ if self.url is None: raise ValueError("Unexpected value None for self.url") @@ -582,16 +579,29 @@ def calendar_multiget(self, event_urls: Iterable[URL]) -> List[_CC]: ) response = self._query(root, 1, "report") results = response.expand_simple_props([cdav.CalendarData()]) - rv = [ - CalendarObjectResource( + for href in response.statuses: + status = response.statuses[href] + if raise_notfound and "404" in status: + raise error.NotFoundError(f"Status {status} in {href}") + return (results[r][cdav.CalendarData.tag] for r in results) + + ## TODO: this is missing test code. + ## TODO: needs refactoring: + ## Objects found may be Todo and Journal, not only Event. + ## Replace the last lines with _request_report_build_resultlist method + def calendar_multiget(self, event_urls: Iterable[URL], raise_notfound: False) -> Iterable[_CC]: + """ + get multiple events' data + @author mtorange@gmail.com + """ + results = self._multiget(event_urls, raise_notfound=raise_notfound) + for res in results: + yield self._calendar_comp_class_by_data(res)( self.client, url=self.url.join(r), - data=results[r][cdav.CalendarData.tag], + data=res, parent=self, ) - for r in results - ] - return rv ## TODO: Upgrade the warning to an error (and perhaps critical) in future ## releases, and then finally remove this method completely. diff --git a/caldav/davclient.py b/caldav/davclient.py index b5a961a7..ef81acb3 100644 --- a/caldav/davclient.py +++ b/caldav/davclient.py @@ -283,6 +283,7 @@ def find_objects_and_props(self) -> Dict[str, Dict[str, _Element]]: self.sync_token will be populated if found, self.objects will be populated. """ self.objects: Dict[str, Dict[str, _Element]] = {} + self.statuses: Dict[str, str] = {} if "Schedule-Tag" in self.headers: self.schedule_tag = self.headers["Schedule-Tag"] @@ -300,6 +301,7 @@ def find_objects_and_props(self) -> Dict[str, Dict[str, _Element]]: ## but then there was https://github.com/python-caldav/caldav/issues/136 if href not in self.objects: self.objects[href] = {} + self.statuses[href] = status ## The properties may be delivered either in one ## propstat with multiple props or in multiple @@ -358,7 +360,7 @@ def _expand_simple_prop( error.assert_(len(values) == 1) return values[0] - ## TODO: "expand" does not feel quite right. + ## TODO: word "expand" does not feel quite right. def expand_simple_props( self, props: Iterable[BaseElement] = None, From 19a4a90a49a6fe2daa6825aade7d2c8c40d34d19 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 22:20:34 +0200 Subject: [PATCH 05/22] hm, this broke all of a sudden, wonder why --- tests/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conf.py b/tests/conf.py index f810e49f..6384040d 100644 --- a/tests/conf.py +++ b/tests/conf.py @@ -127,7 +127,7 @@ def setup_radicale(self): i = 0 while True: try: - niquests.get(self.url) + niquests.get(str(self.url)) break except: time.sleep(0.05) From 1cab06c5b8aa003190dfd3f7e771820242d6fb26 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 22:22:05 +0200 Subject: [PATCH 06/22] hm, this broke all of a sudden, wonder why --- tests/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conf.py b/tests/conf.py index 6384040d..2204132a 100644 --- a/tests/conf.py +++ b/tests/conf.py @@ -207,7 +207,7 @@ def teardown_xandikos(self): ## ... but the thread may be stuck waiting for a request ... def silly_request(): try: - niquests.get(self.url) + niquests.get(str(self.url)) except: pass From e9a7e42371bb4b0134c96ee6c4694baac2b07ff9 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 22:28:09 +0200 Subject: [PATCH 07/22] a pdb breakpoint sneaked into a commit --- tests/test_caldav.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 01f1041e..fa3c78a0 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -2551,8 +2551,6 @@ def testCreateOverwriteDeleteEvent(self): c = self._fixCalendar() assert c.url is not None - import pdb; pdb.set_trace() - # attempts on updating/overwriting a non-existing event should fail (unless object_by_uid_is_broken): if not self.check_compatibility_flag("object_by_uid_is_broken"): with pytest.raises(error.ConsistencyError): From 83545bbdf1ab386b94516d55643b403e928cb181 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 22:30:37 +0200 Subject: [PATCH 08/22] lots of small commits and silly mistakes, perhaps time to go to bed? --- caldav/collection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/caldav/collection.py b/caldav/collection.py index c7c50d3b..db3251f8 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -562,7 +562,7 @@ def save(self): #def data2object_class - def _multiget(self, event_urls: Iterable[URL], raise_notfound=False) -> Iterable[str]: + def _multiget(self, event_urls: Iterable[URL], raise_notfound: bool=False) -> Iterable[str]: """ get multiple events' data @author mtorange@gmail.com @@ -589,7 +589,7 @@ def _multiget(self, event_urls: Iterable[URL], raise_notfound=False) -> Iterable ## TODO: needs refactoring: ## Objects found may be Todo and Journal, not only Event. ## Replace the last lines with _request_report_build_resultlist method - def calendar_multiget(self, event_urls: Iterable[URL], raise_notfound: False) -> Iterable[_CC]: + def calendar_multiget(self, event_urls: Iterable[URL], raise_notfound: bool=False) -> Iterable[_CC]: """ get multiple events' data @author mtorange@gmail.com From fb973881e3024aecf83b3856e563c4520f98bded Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 22:31:04 +0200 Subject: [PATCH 09/22] black style --- caldav/collection.py | 10 +++++++--- tests/test_caldav.py | 2 +- tests/test_caldav_unit.py | 4 +--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/caldav/collection.py b/caldav/collection.py index db3251f8..94476514 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -560,9 +560,11 @@ def save(self): self._create(id=self.id, name=self.name, **self.extra_init_options) return self - #def data2object_class + # def data2object_class - def _multiget(self, event_urls: Iterable[URL], raise_notfound: bool=False) -> Iterable[str]: + def _multiget( + self, event_urls: Iterable[URL], raise_notfound: bool = False + ) -> Iterable[str]: """ get multiple events' data @author mtorange@gmail.com @@ -589,7 +591,9 @@ def _multiget(self, event_urls: Iterable[URL], raise_notfound: bool=False) -> It ## TODO: needs refactoring: ## Objects found may be Todo and Journal, not only Event. ## Replace the last lines with _request_report_build_resultlist method - def calendar_multiget(self, event_urls: Iterable[URL], raise_notfound: bool=False) -> Iterable[_CC]: + def calendar_multiget( + self, event_urls: Iterable[URL], raise_notfound: bool = False + ) -> Iterable[_CC]: """ get multiple events' data @author mtorange@gmail.com diff --git a/tests/test_caldav.py b/tests/test_caldav.py index fa3c78a0..4667e799 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -2550,7 +2550,7 @@ def testCreateOverwriteDeleteEvent(self): # Create calendar c = self._fixCalendar() assert c.url is not None - + # attempts on updating/overwriting a non-existing event should fail (unless object_by_uid_is_broken): if not self.check_compatibility_flag("object_by_uid_is_broken"): with pytest.raises(error.ConsistencyError): diff --git a/tests/test_caldav_unit.py b/tests/test_caldav_unit.py index 3c9d60ad..48ff3a63 100755 --- a/tests/test_caldav_unit.py +++ b/tests/test_caldav_unit.py @@ -281,9 +281,7 @@ def testLoadByMultiGet404(self): """ client = MockedDAVClient(xml) - calendar = Calendar( - client, url="/calendar/issue491/" - ) + calendar = Calendar(client, url="/calendar/issue491/") object = Event(url="/calendar/issue491/notfound.ics", parent=calendar) with pytest.raises(error.NotFoundError): object.load_by_multiget() From 97535041146ebd6e8b8e768e80b10554adf7b3f8 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 22:48:23 +0200 Subject: [PATCH 10/22] tests pass locally now --- caldav/calendarobjectresource.py | 2 +- caldav/collection.py | 47 ++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index eacee67c..ce12bc8f 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -626,7 +626,7 @@ def load_by_multiget(self) -> Self: """ error.assert_(self.url) mydata = self.parent._multiget(event_urls=[self.url], raise_notfound=True) - self.data = next(mydata) + url,self.data = next(mydata) assert_(self.data) assert_(next(mydata, None) is None) return self diff --git a/caldav/collection.py b/caldav/collection.py index 94476514..02886a74 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -566,8 +566,8 @@ def _multiget( self, event_urls: Iterable[URL], raise_notfound: bool = False ) -> Iterable[str]: """ - get multiple events' data - @author mtorange@gmail.com + get multiple events' data. + TODO: Does it overlap the _request_report_build_resultlist method """ if self.url is None: raise ValueError("Unexpected value None for self.url") @@ -581,32 +581,39 @@ def _multiget( ) response = self._query(root, 1, "report") results = response.expand_simple_props([cdav.CalendarData()]) - for href in response.statuses: - status = response.statuses[href] - if raise_notfound and "404" in status: - raise error.NotFoundError(f"Status {status} in {href}") - return (results[r][cdav.CalendarData.tag] for r in results) - - ## TODO: this is missing test code. - ## TODO: needs refactoring: - ## Objects found may be Todo and Journal, not only Event. - ## Replace the last lines with _request_report_build_resultlist method - def calendar_multiget( - self, event_urls: Iterable[URL], raise_notfound: bool = False - ) -> Iterable[_CC]: + if raise_notfound: + for href in response.statuses: + status = response.statuses[href] + if "404" in status: + raise error.NotFoundError(f"Status {status} in {href}") + for r in results: + yield (r,results[r][cdav.CalendarData.tag]) + + ## Replace the last lines with + def multiget(self, event_urls: Iterable[URL], raise_notfound: bool=False) -> Iterable[_CC]: """ get multiple events' data - @author mtorange@gmail.com + TODO: Does it overlap the _request_report_build_resultlist method? + @author mtorange@gmail.com (refactored by Tobias) """ results = self._multiget(event_urls, raise_notfound=raise_notfound) - for res in results: - yield self._calendar_comp_class_by_data(res)( + for url,data in results: + yield self._calendar_comp_class_by_data(data)( self.client, - url=self.url.join(r), - data=res, + url=self.url.join(url), + data=data, parent=self, ) + def calendar_multiget(self, *largs, **kwargs): + """ + get multiple events' data + @author mtorange@gmail.com + (refactored by Tobias) + This is for backward compatibility. It may be removed in 3.0 or later release. + """ + return list(self.multiget(*largs, **kwargs)) + ## TODO: Upgrade the warning to an error (and perhaps critical) in future ## releases, and then finally remove this method completely. def build_date_search_query( From 491016e2550eb83f7081b393f862c6fdfcb88c0d Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 18 May 2025 22:50:38 +0200 Subject: [PATCH 11/22] black --- caldav/calendarobjectresource.py | 2 +- caldav/collection.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index ce12bc8f..aefa2706 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -626,7 +626,7 @@ def load_by_multiget(self) -> Self: """ error.assert_(self.url) mydata = self.parent._multiget(event_urls=[self.url], raise_notfound=True) - url,self.data = next(mydata) + url, self.data = next(mydata) assert_(self.data) assert_(next(mydata, None) is None) return self diff --git a/caldav/collection.py b/caldav/collection.py index 02886a74..5f2b8ccf 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -587,17 +587,19 @@ def _multiget( if "404" in status: raise error.NotFoundError(f"Status {status} in {href}") for r in results: - yield (r,results[r][cdav.CalendarData.tag]) + yield (r, results[r][cdav.CalendarData.tag]) - ## Replace the last lines with - def multiget(self, event_urls: Iterable[URL], raise_notfound: bool=False) -> Iterable[_CC]: + ## Replace the last lines with + def multiget( + self, event_urls: Iterable[URL], raise_notfound: bool = False + ) -> Iterable[_CC]: """ get multiple events' data TODO: Does it overlap the _request_report_build_resultlist method? @author mtorange@gmail.com (refactored by Tobias) """ results = self._multiget(event_urls, raise_notfound=raise_notfound) - for url,data in results: + for url, data in results: yield self._calendar_comp_class_by_data(data)( self.client, url=self.url.join(url), From 47262ca835be65f545eb6c1d0c907d1ad9941bd2 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Mon, 19 May 2025 22:37:37 +0200 Subject: [PATCH 12/22] tweaks to get tests passing on gmx.net --- caldav/compatibility_hints.py | 6 ++++++ tests/test_caldav.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index 7df73aee..87ee7ea8 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -77,6 +77,9 @@ 'no_scheduling_mailbox': """Parts of RFC6833 is supported, but not the existence of inbox/mailbox""", + 'no_scheduling_calendar_user_address_set': + """Parts of RFC6833 is supported, but not getting the calendar users addresses""", + 'no_default_calendar': """The given user starts without an assigned default calendar """ """(or without pre-defined calendars at all)""", @@ -496,6 +499,7 @@ 'no_relships', 'isnotdefined_not_working', 'no_alarmsearch', + 'broken_expand', ] posteo = [ @@ -545,6 +549,8 @@ "no_freebusy_rfc4791", "no_expand", "no_search_openended", + "no_sync_token", + "no_scheduling_calendar_user_address_set", #"broken_expand_on_exceptions", ## should be implied by no_expand? ] diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 4667e799..229f036f 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -751,6 +751,7 @@ def testSupport(self): def testSchedulingInfo(self): self.skip_on_compatibility_flag("no_scheduling") + self.skip_on_compatibility_flag("no_scheduling_calendar_user_address_set") calendar_user_address_set = self.principal.calendar_user_address_set() me_a_participant = self.principal.get_vcal_address() @@ -2828,6 +2829,7 @@ def testRecurringDateWithExceptionSearch(self): c = self._fixCalendar() # evr2 is a bi-weekly event starting 2024-04-11 + ## It has an exception, edited summary for recurrence id 20240425T123000Z e = c.save_event(evr2) r = c.search( From d423bd6602ad60251a3274db9ea0f50ac7fb60bb Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Tue, 20 May 2025 07:12:31 +0200 Subject: [PATCH 13/22] some work to get tests passing on calendar servers offering only one calendar and no mkcalendar. Tweaking the compatibility matrixes --- caldav/calendarobjectresource.py | 5 ----- caldav/compatibility_hints.py | 9 +++++++-- tests/test_caldav.py | 20 ++++++++++++++------ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index aefa2706..e49dffab 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -398,11 +398,6 @@ def fix_reverse_relations(self, pdb: bool = False) -> list: """ return self._handle_reverse_relations(verify=True, fix=True, pdb=pdb) - ## TODO: fix this (and consolidate with _handle_relations / set_relation?) - # def ensure_reverse_relations(self): - # missing_relations = self.check_reverse_relations() - # ... - def _get_icalendar_component(self, assert_one=False): """Returns the icalendar subcomponent - which should be an Event, Journal, Todo or FreeBusy from the icalendar class diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index 87ee7ea8..cee14150 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -509,6 +509,7 @@ 'no_recurring_todo', 'no_sync_token', 'combined_search_not_working', + 'no_alarmsearch', 'broken_expand', ] @@ -538,19 +539,23 @@ ## Purelymail claims that the search indexes are "lazily" populated, ## so search works some minutes after the event was created/edited. - 'search_delay' + 'search_delay', + + ## I haven't raised this one with them yet + 'no_alarmsearch', ] gmx = [ "no_scheduling_mailbox", "no_mkcalendar", "search_needs_comptype", - "text_search_is_case_insensitive", + #"text_search_is_case_insensitive", "no_freebusy_rfc4791", "no_expand", "no_search_openended", "no_sync_token", "no_scheduling_calendar_user_address_set", + "rate_limited", #"broken_expand_on_exceptions", ## should be implied by no_expand? ] diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 229f036f..b0c1660e 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -136,6 +136,8 @@ "test4", "test5", "test6", + "c26921f4-0653-11ef-b756-58ce2a14e2e5", + "e2a2e13e-34f2-11f0-ae12-1c1bb5134174", ) ## TODO: todo7 is an item without uid. Should be taken care of somehow. @@ -554,7 +556,7 @@ def testInviteAndRespond(self): ## inbox/outbox? -def _delay_decorator(f, t=10): +def _delay_decorator(f, t=20): def foo(*a, **kwa): time.sleep(t) return f(*a, **kwa) @@ -968,6 +970,8 @@ def testCreateEvent(self): c = self._fixCalendar() existing_events = c.events() + existing_urls = {x.url for x in existing_events} + cleanse = lambda events: [x for x in events if x.url not in existing_urls] if not self.check_compatibility_flag("no_mkcalendar"): ## we're supposed to be working towards a brand new calendar @@ -977,13 +981,13 @@ def testCreateEvent(self): c.save_event(broken_ev1) # c.events() should give a full list of events - events = c.events() - assert len(events) == len(existing_events) + 1 + events = cleanse(c.events()) + assert len(events) == 1 # We should be able to access the calender through the URL c2 = self.caldav.calendar(url=c.url) - events2 = c2.events() - assert len(events2) == len(existing_events) + 1 + events2 = cleanse(c2.events()) + assert len(events2) == 1 assert events2[0].url == events[0].url if not self.check_compatibility_flag( @@ -994,7 +998,7 @@ def testCreateEvent(self): ## may break if we have multiple calendars with the same name if not self.check_compatibility_flag("no_delete_calendar"): assert c2.url == c.url - events2 = c2.events() + events2 = cleanse(c2.events()) assert len(events2) == 1 assert events2[0].url == events[0].url @@ -1015,6 +1019,7 @@ def testAlarm(self): ev = c.save_event( dtstart=datetime(2015, 10, 10, 8, 0, 0), summary="This is a test event", + uid="test1", dtend=datetime(2016, 10, 10, 9, 0, 0), alarm_trigger=timedelta(minutes=-15), alarm_action="AUDIO", @@ -1910,6 +1915,9 @@ def testSetDue(self): child=[some_todo.id], ) + + assert not parent.check_reverse_relations() + ## The above updates the some_todo object on the server side, but the local object is not ## updated ... until we reload it some_todo.load() From 608a541737792b4b6146ebe9aac6a23fc49e70ac Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Tue, 20 May 2025 19:25:39 +0200 Subject: [PATCH 14/22] downgrading the path handling problem error to a warning + a hack for GMX needed some update --- caldav/compatibility_hints.py | 2 -- caldav/davobject.py | 12 ++++++------ caldav/lib/error.py | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index cee14150..87330ac3 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -555,8 +555,6 @@ "no_search_openended", "no_sync_token", "no_scheduling_calendar_user_address_set", - "rate_limited", - #"broken_expand_on_exceptions", ## should be implied by no_expand? ] # fmt: on diff --git a/caldav/davobject.py b/caldav/davobject.py index 01cca546..d4e7309a 100644 --- a/caldav/davobject.py +++ b/caldav/davobject.py @@ -216,11 +216,11 @@ def _query( body = to_wire(body) if ( ret.status == 500 - and b"getetag" not in body - and b"" in body + and b"D:getetag" not in body + and b"", b"" + b" None: raise -ERR_FRAGMENT: str = "Please consider raising an issue at https://github.com/python-caldav/caldav/issues or reach out to t-caldav@tobixen.no, include this error and the traceback and tell what server you are using" +ERR_FRAGMENT: str = "Please consider raising an issue at https://github.com/python-caldav/caldav/issues or reach out to t-caldav@tobixen.no, include this error and the traceback (if any) and tell what server you are using" class DAVError(Exception): From 689075ca157a9be3f063e1e0ec78878944584cfc Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Tue, 20 May 2025 20:08:44 +0200 Subject: [PATCH 15/22] style fix --- tests/test_caldav.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_caldav.py b/tests/test_caldav.py index b0c1660e..6aeb9577 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -1915,7 +1915,6 @@ def testSetDue(self): child=[some_todo.id], ) - assert not parent.check_reverse_relations() ## The above updates the some_todo object on the server side, but the local object is not From 0e0fe6278ffb99bcb0250eba9cb5b682769a6a4e Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Tue, 20 May 2025 23:33:12 +0200 Subject: [PATCH 16/22] another workaround for exotic server implementations --- caldav/calendarobjectresource.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index e49dffab..89eff261 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -621,7 +621,14 @@ def load_by_multiget(self) -> Self: """ error.assert_(self.url) mydata = self.parent._multiget(event_urls=[self.url], raise_notfound=True) - url, self.data = next(mydata) + try: + url, self.data = next(mydata) + except StopIteration: + ## We shouldn't come here. Something is wrong. + ## TODO: research it + ## As of 2025-05-20, this code section is used by + ## TestForServerECloud::testCreateOverwriteDeleteEvent + raise error.NotFoundError(self.url) assert_(self.data) assert_(next(mydata, None) is None) return self From e4cf1e0838d643b50b7746e6fc8350839711c364 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Tue, 20 May 2025 23:33:50 +0200 Subject: [PATCH 17/22] style --- caldav/calendarobjectresource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index 89eff261..c5b96a7e 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -627,7 +627,7 @@ def load_by_multiget(self) -> Self: ## We shouldn't come here. Something is wrong. ## TODO: research it ## As of 2025-05-20, this code section is used by - ## TestForServerECloud::testCreateOverwriteDeleteEvent + ## TestForServerECloud::testCreateOverwriteDeleteEvent raise error.NotFoundError(self.url) assert_(self.data) assert_(next(mydata, None) is None) From 5146aab27ff1116bfa6fb369f28206f825bcac4b Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Wed, 21 May 2025 11:32:42 +0200 Subject: [PATCH 18/22] test framework: cleanse output from the calendar of existing stuff --- tests/test_caldav.py | 53 +++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 6aeb9577..9f70f223 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -1416,6 +1416,8 @@ def testSearchEvent(self): self.skip_on_compatibility_flag("no_search") c = self._fixCalendar() + num_existing = len(c.events()) + c.save_event(ev1) c.save_event(ev3) c.save_event(evr) @@ -1423,13 +1425,13 @@ def testSearchEvent(self): ## Search without any parameters should yield everything on calendar all_events = c.search() if self.check_compatibility_flag("search_needs_comptype"): - assert len(all_events) <= 3 + assert len(all_events) <= 3 + num_existing else: - assert len(all_events) == 3 + assert len(all_events) == 3 + num_existing ## Search with comp_class set to Event should yield all events on calendar all_events = c.search(comp_class=Event) - assert len(all_events) == 3 + assert len(all_events) == 3 + num_existing ## Search with todo flag set should yield no events try: @@ -1595,6 +1597,9 @@ def testSearchSortTodo(self): self.skip_on_compatibility_flag("no_todo") self.skip_on_compatibility_flag("no_search") c = self._fixCalendar(supported_calendar_component_set=["VTODO"]) + pre_todos = c.todos() + pre_todo_uid_map = {x.icalendar_component['uid'] for x in pre_todos} + cleanse = lambda tasks: [x for x in tasks if x.icalendar_component['uid'] not in pre_todo_uid_map] t1 = c.save_todo( summary="1 task overdue", due=date(2022, 12, 12), @@ -1634,15 +1639,15 @@ def check_order(tasks, order): "test" + str(x) for x in order ] - all_tasks = c.search(todo=True, sort_keys=("uid",)) + all_tasks = cleanse(c.search(todo=True, sort_keys=("uid",))) check_order(all_tasks, (1, 2, 3, 4, 6)) - all_tasks = c.search(sort_keys=("summary",)) + all_tasks = cleanse(c.search(sort_keys=("summary",))) check_order(all_tasks, (1, 2, 3, 4, 5, 6)) - all_tasks = c.search( + all_tasks = cleanse(c.search( sort_keys=("isnt_overdue", "categories", "dtstart", "priority", "status") - ) + )) ## This is difficult ... ## * 1 is the only one that is overdue, and False sorts before True, so 1 comes first ## * categories, empty string sorts before a non-empty string, so 6 is at the end of the list @@ -1657,6 +1662,8 @@ def testSearchTodos(self): self.skip_on_compatibility_flag("no_search") c = self._fixCalendar(supported_calendar_component_set=["VTODO"]) + pre_cnt = len(c.todos()) + t1 = c.save_todo(todo) t2 = c.save_todo(todo2) t3 = c.save_todo(todo3) @@ -1667,13 +1674,13 @@ def testSearchTodos(self): ## Search without any parameters should yield everything on calendar all_todos = c.search() if self.check_compatibility_flag("search_needs_comptype"): - assert len(all_todos) <= 6 + assert len(all_todos) <= 6 + pre_cnt else: - assert len(all_todos) == 6 + assert len(all_todos) == 6 + pre_cnt ## Search with comp_class set to Event should yield all events on calendar all_todos = c.search(comp_class=Event) - assert len(all_todos) == 0 + assert len(all_todos) == 0 + pre_cnt ## Search with todo flag set should yield all 6 tasks ## (Except, if the calendar server does not support is-not-defined very @@ -1681,20 +1688,20 @@ def testSearchTodos(self): ## https://gitlab.com/davical-project/davical/-/issues/281 ) all_todos = c.search(todo=True) if self.check_compatibility_flag("isnotdefined_not_working"): - assert len(all_todos) in (3, 6) + assert len(all_todos) - pre_cnt in (3, 6) else: - assert len(all_todos) == 6 + assert len(all_todos) == 6 + pre_cnt ## Search for misc text fields ## UID is a special case, supported by almost all servers some_todos = c.search(comp_class=Todo, uid="19970901T130000Z-123404@host.com") if not self.check_compatibility_flag("text_search_not_working"): - assert len(some_todos) == 1 + assert len(some_todos) == 1 + pre_cnt ## class ... hm, all 6 example todos are 'CONFIDENTIAL' ... some_todos = c.search(comp_class=Todo, class_="CONFIDENTIAL") if not self.check_compatibility_flag("text_search_not_working"): - assert len(some_todos) == 6 + assert len(some_todos) == 6 + pre_cnt ## category ## Too much copying of the examples ... @@ -1702,34 +1709,34 @@ def testSearchTodos(self): if not self.check_compatibility_flag( "category_search_yields_nothing" ) and not self.check_compatibility_flag("text_search_not_working"): - assert len(some_todos) == 6 + assert len(some_todos) == 6 + pre_cnt some_todos = c.search(comp_class=Todo, category="finance") if not self.check_compatibility_flag( "category_search_yields_nothing" ) and not self.check_compatibility_flag("text_search_not_working"): if self.check_compatibility_flag("text_search_is_case_insensitive"): - assert len(some_todos) == 6 + assert len(some_todos) == 6 + pre_cnt else: - assert len(some_todos) == 0 + assert len(some_todos) == 0 + pre_cnt ## This is not a very useful search, and it's sort of a client side bug that we allow it at all. ## It will not match if categories field is set to "PERSONAL,ANNIVERSARY,SPECIAL OCCASION" ## It may not match since the above is to be considered equivalent to the raw data entered. some_todos = c.search(comp_class=Todo, category="FAMILY,FINANCE") if not self.check_compatibility_flag("text_search_not_working"): - assert len(some_todos) in (0, 6) + assert len(some_todos) - pre_cnt in (0, 6) ## TODO: We should consider to do client side filtering to ensure exact ## match only on components having MIL as a category (and not FAMILY) some_todos = c.search(comp_class=Todo, category="MIL") if self.check_compatibility_flag("text_search_is_exact_match_sometimes"): - assert len(some_todos) in (0, 6) + assert len(some_todos) - pre_cnt in (0, 6) elif self.check_compatibility_flag("text_search_is_exact_match_only"): - assert len(some_todos) == 0 + assert len(some_todos) - pre_cnt == 0 elif not self.check_compatibility_flag( "category_search_yields_nothing" ) and not self.check_compatibility_flag("text_search_not_working"): ## This is the correct thing, according to the letter of the RFC - assert len(some_todos) == 6 + assert len(some_todos) - pre_cnt == 6 ## completing events, and it should not show up anymore t3.complete() @@ -1737,11 +1744,11 @@ def testSearchTodos(self): t6.complete() some_todos = c.search(todo=True) - assert len(some_todos) == 3 + assert len(some_todos) == 3 + pre_cnt ## unless we specifically ask for completed tasks all_todos = c.search(todo=True, include_completed=True) - assert len(all_todos) == 6 + assert len(all_todos) == 6 + pre_cnt def testWrongPassword(self): if ( From a6d35f98d63a341d9affbfbb54be9666031f2b35 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Wed, 21 May 2025 11:37:27 +0200 Subject: [PATCH 19/22] changelog and a bugfix --- CHANGELOG.md | 37 +++++++++++++++++++++----------- caldav/calendarobjectresource.py | 2 +- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc364fde..0b13d565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,25 +8,41 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 ## [2.0.0] - [Unreleased] -In version 2.0, the requests library will be replaced with niquests or httpx. See https://github.com/python-caldav/caldav/issues/457. Master branch is currently running niquests. +In version 2.0, the requests library will be replaced with niquests or httpx. See https://github.com/python-caldav/caldav/issues/457. Master branch is currently running niquests. Work by @ArtemIsmagilov, https://github.com/python-caldav/caldav/pull/455 -In version 2.0, support for python 3.7 will be dropped, possibly also python 3.8. Master branch supports both as for now. +In version 2.0, support for python 3.7 and python 3.8 will be officially dropped. Master branch *should* support both as for now, but Python 3.7 is no longer tested. ## [1.5.0] - [Unreleased] ### Deprecated -Python 3.7 is no longer tested - but it should work. Please file a bug report if it doesn't work. (Note that the caldav library pulls in many dependencies, and not all of them supports dead snakes). +Python 3.7 is no longer tested (dependency problems) - but it should work. Please file a bug report if it doesn't work. (Note that the caldav library pulls in many dependencies, and not all of them supports dead snakes). -### Changed +### Fixed +* Servers that return a quoted URL in their path will now be parsed correctly by @edel-macias-cubix in https://github.com/python-caldav/caldav/pull/473 +* Compatibility workaround: If `event.load()` fails, it will retry the load by doing a multiget - https://github.com/python-caldav/caldav/pull/460 and https://github.com/python-caldav/caldav/pull/475 - https://github.com/python-caldav/caldav/issues/459 +* Compatibility workaround: A problem with a wiki calendar fixed by @soundstorm in https://github.com/python-caldav/caldav/pull/469 +* Blank passwords should be acceptable - https://github.com/python-caldav/caldav/pull/481 +* Compatibility workaround: Accept XML content from calendar server even if it's marked up with content-type text/plain by @niccokunzmann in https://github.com/python-caldav/caldav/pull/465 +* Bugfix for saving component failing on multi-component recurrence objects - https://github.com/python-caldav/caldav/pull/467 * Some exotic servers may return object URLs on search, but it does not work out to fetch the calendar data. Now it will log an error instead of raising an error in such cases. + +### Changed + * The `tests/compatibility_issues.py` has been moved to `caldav/compatibility_hints.py`, this to make it available for a caldav-server-tester-tool that I'm splitting off to a separate project/repository, and also to make https://github.com/python-caldav/caldav/issues/402 possible. #### Refactoring * Minor code cleanups by github user @ArtemIsmagilov in https://github.com/python-caldav/caldav/pull/456 -* The very much overgrown `objects.py`-file has been split into three. +* The very much overgrown `objects.py`-file has been split into three - https://github.com/python-caldav/caldav/pull/483 +* Refactor compatibility issues by @tobixen in https://github.com/python-caldav/caldav/pull/484 + +### Documentation + +* Add more project links to PyPI by @niccokunzmann in https://github.com/python-caldav/caldav/pull/464 +* Document how to use tox for testing by @niccokunzmann in https://github.com/python-caldav/caldav/pull/466 +* Readthedocs integration has been repaired (https://github.com/python-caldav/caldav/pull/453 - but eventually the fix was introduced directly in the master branch) #### Test framework @@ -42,17 +58,14 @@ Python 3.7 is no longer tested - but it should work. Please file a bug report i ### Added +* support easy search for journals by @tobixen in https://github.com/python-caldav/caldav/pull/486 +* Methods for verifying and adding reverse relations - https://github.com/python-caldav/caldav/pull/336 +* Easy creation of events and tasks with alarms, search for alarms - https://github.com/python-caldav/caldav/pull/221 * Work in progress: `auto_conn`, `auto_calendar` and `auto_calendars` may read caldav connection and calendar configuration from a config file, environmental variables or other sources. Currently I've made the minimal possible work to be able to test the caldav-server-tester script. * By now `calendar.search(..., sort_keys=("DTSTART")` will work. Sort keys expects a list or a tuple, but it's easy to send an attribute by mistake. https://github.com/python-caldav/caldav/issues/448 https://github.com/python-caldav/caldav/pull/449 -* Compatibility workaround: If `event.load()` fails, it will retry the load by doing a multiget - https://github.com/python-caldav/caldav/pull/475 - https://github.com/python-caldav/caldav/issues/459 * The `class_`-parameter now works when sending data to `save_event()` etc. * Search method now takes parameter `journal=True`. ref https://github.com/python-caldav/caldav/issues/237 and https://github.com/python-caldav/caldav/pull/486 -### Fixed - -* Bugfix for saving component failing on multi-component recurrence objects - https://github.com/python-caldav/caldav/pull/467 -* Readthedocs integration has been repaired (https://github.com/python-caldav/caldav/pull/453 - but eventually the fix was introduced directly in the master branch) - ## [1.4.0] - 2024-11-05 @@ -86,8 +99,6 @@ Python 3.7 is no longer tested - but it should work. Please file a bug report i ### Added -* Methods for verifying and adding reverse relations - https://github.com/python-caldav/caldav/pull/336 -* Easy creation of events and tasks with alarms, search for alarms - https://github.com/python-caldav/caldav/pull/221 * Allow to reverse the sorting order on search function by @twissell- in https://github.com/python-caldav/caldav/pull/433 * Work on integrating typing information. Details in https://github.com/python-caldav/caldav/pull/358 * Remove dependency on pytz. Details in https://github.com/python-caldav/caldav/issues/231 and https://github.com/python-caldav/caldav/pull/363 diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index c5b96a7e..5a660912 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -606,7 +606,7 @@ def load(self, only_if_unloaded: bool = False) -> Self: if r.status == 404: raise error.NotFoundError(errmsg(r)) except: - return self.load_by_multiget() + self.load_by_multiget() self.data = vcal.fix(r.raw) if "Etag" in r.headers: self.props[dav.GetEtag.tag] = r.headers["Etag"] From 02489f66bed54221634c614f52566d11184186f7 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Wed, 21 May 2025 11:37:53 +0200 Subject: [PATCH 20/22] style --- tests/test_caldav.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 9f70f223..54e54c2b 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -1598,8 +1598,10 @@ def testSearchSortTodo(self): self.skip_on_compatibility_flag("no_search") c = self._fixCalendar(supported_calendar_component_set=["VTODO"]) pre_todos = c.todos() - pre_todo_uid_map = {x.icalendar_component['uid'] for x in pre_todos} - cleanse = lambda tasks: [x for x in tasks if x.icalendar_component['uid'] not in pre_todo_uid_map] + pre_todo_uid_map = {x.icalendar_component["uid"] for x in pre_todos} + cleanse = lambda tasks: [ + x for x in tasks if x.icalendar_component["uid"] not in pre_todo_uid_map + ] t1 = c.save_todo( summary="1 task overdue", due=date(2022, 12, 12), @@ -1645,9 +1647,17 @@ def check_order(tasks, order): all_tasks = cleanse(c.search(sort_keys=("summary",))) check_order(all_tasks, (1, 2, 3, 4, 5, 6)) - all_tasks = cleanse(c.search( - sort_keys=("isnt_overdue", "categories", "dtstart", "priority", "status") - )) + all_tasks = cleanse( + c.search( + sort_keys=( + "isnt_overdue", + "categories", + "dtstart", + "priority", + "status", + ) + ) + ) ## This is difficult ... ## * 1 is the only one that is overdue, and False sorts before True, so 1 comes first ## * categories, empty string sorts before a non-empty string, so 6 is at the end of the list From 2228e4d26156f29c462f64e06acd7a1020e45f2f Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Wed, 21 May 2025 11:43:54 +0200 Subject: [PATCH 21/22] CHANGELOG entries, making ready for v1.5-release --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b13d565..d5ecfef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ Python 3.7 is no longer tested (dependency problems) - but it should work. Plea * Allows offline testing of my upcoming `check_server_compatibility`-script * Also added the possibility to tag test servers with a name * Many changes done to the compatibility flag list (due to work on the server-checker project) +* Functional tests for multiget in https://github.com/python-caldav/caldav/pull/489 ### Added From ab0bbd1f0776127c064ffce925f866077ebf2a19 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Wed, 21 May 2025 12:14:35 +0200 Subject: [PATCH 22/22] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5ecfef8..69cdb5e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Python 3.7 is no longer tested (dependency problems) - but it should work. Plea * Compatibility workaround: Accept XML content from calendar server even if it's marked up with content-type text/plain by @niccokunzmann in https://github.com/python-caldav/caldav/pull/465 * Bugfix for saving component failing on multi-component recurrence objects - https://github.com/python-caldav/caldav/pull/467 * Some exotic servers may return object URLs on search, but it does not work out to fetch the calendar data. Now it will log an error instead of raising an error in such cases. +* Some workarounds and fixes for getting tests passing on all the test servers I had at hand in https://github.com/python-caldav/caldav/pull/492 ### Changed @@ -37,6 +38,7 @@ Python 3.7 is no longer tested (dependency problems) - but it should work. Plea * Minor code cleanups by github user @ArtemIsmagilov in https://github.com/python-caldav/caldav/pull/456 * The very much overgrown `objects.py`-file has been split into three - https://github.com/python-caldav/caldav/pull/483 * Refactor compatibility issues by @tobixen in https://github.com/python-caldav/caldav/pull/484 +* Refactoring of `multiget` in https://github.com/python-caldav/caldav/pull/492 ### Documentation