Skip to content

Commit da40a68

Browse files
committed
refactored to use the new icalendar-searcher package
1 parent 412e40f commit da40a68

3 files changed

Lines changed: 63 additions & 108 deletions

File tree

‎caldav/collection.py‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -804,9 +804,9 @@ def search(
804804
results if needed and returns the objects found.
805805
806806
Refactoring 2025-11: a new class
807-
class:`caldav.search.ComponentSearcher` has been made, and
807+
class:`caldav.search.CalDAVSearcher` has been made, and
808808
this method is sort of a wrapper for
809-
ComponentSearcher.search_caldav, ensuring backward
809+
CalDAVSearcher.search, ensuring backward
810810
compatibility. The documentation may be slightly overlapping.
811811
812812
I believe that for simple tasks, this method will be easier to
@@ -815,7 +815,7 @@ def search(
815815
continue working as it has been doing before for all
816816
foreseeable future. I believe that for simple tasks, this
817817
method will be easier to use than to construct a
818-
ComponentSearcher object and do searches from there. The
818+
CalDAVSearcher object and do searches from there. The
819819
refactoring was made necessary because the parameter list to
820820
`search` was becoming unmanagable. Advanced searches should
821821
be done via the new interface.
@@ -887,11 +887,11 @@ def search(
887887
888888
"""
889889
## Late import to avoid cyclic imports
890-
from .search import ComponentSearcher
890+
from .search import CalDAVSearcher
891891

892-
## This is basically a wrapper for ComponentSearcher.search_caldav
892+
## This is basically a wrapper for CalDAVSearcher.search
893893
## The logic below will massage the parameters in ``searchargs``
894-
## and put them into the ComponentSearcher object.
894+
## and put them into the CalDAVSearcher object.
895895

896896
if searchargs.get("expand", True) not in (True, False):
897897
warnings.warn(
@@ -905,8 +905,8 @@ def search(
905905
server_expand = True
906906
searchargs["expand"] = False
907907

908-
## Transfer all the arguments to ComponentSearcher
909-
my_searcher = ComponentSearcher()
908+
## Transfer all the arguments to CalDAVSearcher
909+
my_searcher = CalDAVSearcher()
910910
for key in searchargs:
911911
assert key[0] != "_" ## not allowed
912912
alias = key
@@ -937,7 +937,7 @@ def search(
937937
if not xml and filters:
938938
xml = filters
939939

940-
return my_searcher.search_caldav(
940+
return my_searcher.search(
941941
self, server_expand, split_expanded, props, xml, _hacks
942942
)
943943

@@ -1053,7 +1053,7 @@ def object_by_uid(
10531053
# uid given in the query is short (i.e. just "0") we're likely to
10541054
# get false positives back from the server, we need to do an extra
10551055
# check that the uid is correct
1056-
## Todo: make support for "full text search" in the ComponentSearcher,
1056+
## Todo: make support for "full text search" in the CalDAVSearcher,
10571057
## and move the filtering logic there
10581058
items_found2 = []
10591059
for item in items_found:

‎caldav/search.py‎

Lines changed: 51 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import List
88
from typing import Optional
99

10+
from icalendar_searcher import Searcher
1011
from icalendar.prop import TypesFactory
1112
from lxml import etree
1213

@@ -23,93 +24,49 @@
2324
TypesFactory = TypesFactory()
2425

2526

26-
@dataclass
27-
class ComponentSearcher:
28-
"""The primary purpose of this class is to bundle together all
29-
search filters plus sort options to be used in calendar searches.
30-
I also have long-term plans to allow for comparative filtering,
31-
logical OR, etc. Things that are not supported by the CalDAV
32-
protocol can still be done client-side. Over time I'm going to
33-
support other protocols that may or may not support more advanced
34-
searches.
27+
class CalDAVSearcher(Searcher):
28+
"""The baseclass (which is generic, and not CalDAV-specific)
29+
allows building up a search query search logic.
3530
36-
For simple searches, the old way to do it will always work:
37-
``calendar.search(from=..., to=..., ...)``
38-
This class offers an alternative way, this should be equivalent:
39-
``ComponentSearchFilter(from=..., to=...).search(calendar)``
31+
The base class also allows for simple client-side filtering (and
32+
at some point in the future, more complex client-side filtering).
4033
41-
icalendar properties are not meant to be sent through the
42-
constructor, use the ``add_property_filter`` method. Same
43-
goes with sort keys, they can be added through the ``add_sort_key`` method.
34+
The CalDAV protocol is difficult, ambigiuous and does not offer
35+
all kind of searches. Client-side filtering may be needed to
36+
smoothen over differences in how the different servers handle
37+
search queries, as well as allowing for more complex searches.
4438
45-
The ``todo``, ``event`` and ``journal`` parameters are booleans
46-
for filtering the component type. It's currently recommended to
47-
set one and only one of them to True. If i.e. both todo and
48-
journal is set to True, everything but events should be returned,
49-
but don't expect this to work as of 2025-11. If none er given
50-
(the default), all objects should be returned. The latter depends
51-
either on the server implementation or that the correct
52-
``compatibility_hints`` is configured for the caldav server.
39+
A search may be performed by first setting up a CalDAVSearcher,
40+
populate it with filter options, and then initiate the search from
41+
he CalDAVSearcher. Something like this (see the doc in the base
42+
class):
5343
54-
For ``todo``, ``include_completed`` defaults to None, adding
55-
logic for filtering out those.
56-
57-
``start`` and ``end`` is giving a time range as defined in
58-
RFC4791, section 9.9. Note that those must be timestamps
59-
and cannot be dates (as for now).
60-
61-
``alarm_start`` and ``alarm_end`` is similar for alarm searching
44+
``ComponentSearchFilter(from=..., to=...).search(calendar)``
6245
63-
If ``expand`` is set to True, recurring objects will be expanded
64-
into reccurence objects. This is only applicable if used together
65-
with both ``start`` and ``end``. Recommended.
46+
However, for simple searches, the old way to
47+
do it will always work:
48+
49+
``calendar.search(from=..., to=..., ...)``
50+
51+
The ``todo``, ``event`` and ``journal`` parameters are booleans
52+
for filtering the component type. It's currently recommended to
53+
set one and only one of them to True, as of 2025-11 there is no
54+
guarantees for correct behaviour if setting two of them. Also, if
55+
none is given (the default), all objects should be returned -
56+
however, all examples in the CalDAV RFC filters things by
57+
component, and the different servers do different things when
58+
confronted with a search missing component type. With the correct
59+
``compatibility_hints`` (``davclient.features``) configured for
60+
the caldav server, the algorithms will ensure correct behaviour.
61+
62+
Both the iCalendar standard and the (Cal)DAV standard defines
63+
"properties". Make sure not to confuse those. iCalendar
64+
properties used for filtering can be passed using
65+
``searcher.add_property_filter``.
6666
"""
6767

68-
todo: bool = None
69-
event: bool = None
70-
journal: bool = None
71-
start: datetime = None
72-
end: datetime = None
73-
alarm_start: datetime = None
74-
alarm_end: datetime = None
75-
comp_class: CalendarObjectResource = None
76-
include_completed: bool = None
77-
78-
expand: bool = False
79-
80-
_sort_keys: list = field(default_factory=list)
81-
_property_filters: dict = field(default_factory=dict)
82-
_property_operator: dict = field(default_factory=dict)
83-
84-
def add_property_filter(self, key: str, value: Any, operator: str = "contains") -> None:
85-
"""Adds a filter for some specific iCalendar property.
8668

87-
An iCalendar property should not be confused with a CalDAV
88-
property. Examples of valid iCalendar properties: SUMMARY,
89-
LOCATION, DESCRIPTION, DTSTART, STATUS, CLASS, etc
90-
91-
:param key: must be an icalendar property, i.e. SUMMARY
92-
:param value: must adhere to the type defined in the RFC
93-
:param operator: only == supported as for now
94-
95-
"""
96-
## some day in the future, perhaps we'll implement support for comparative operators ...
97-
assert operator in ("contains", "undef")
98-
if operator != "undef":
99-
self._property_filters[key] = TypesFactory.for_property(key)(value)
100-
self._property_operator[key] = operator
101-
102-
def add_sort_key(self, key: str, reversed: bool = None) -> None:
103-
"""
104-
The sort key should be an icalendar property.
105-
"""
106-
assert key in TypesFactory.types_map or key in ("isnt_overdue", "hasnt_started")
107-
self._sort_keys.append((key, reversed))
108-
109-
def filter(*args, **kwargs):
110-
raise NotImplementedError()
111-
112-
def _search_caldav_with_comptypes(
69+
def _search_with_comptypes(
11370
self,
11471
calendar: Calendar,
11572
server_expand: bool = False,
@@ -129,14 +86,14 @@ def _search_caldav_with_comptypes(
12986
objects = []
13087
for comp_class in (Event, Todo, Journal):
13188
clone.comp_class = comp_class
132-
objects += clone.search_caldav(
89+
objects += clone.search(
13390
calendar, server_expand, split_expanded, props, xml
13491
)
13592
self.sort_objects(objects)
13693
return objects
13794

13895
## TODO: refactor, split more logic out in smaller methods
139-
def search_caldav(
96+
def search(
14097
self,
14198
calendar: Calendar,
14299
server_expand: bool = False,
@@ -148,9 +105,9 @@ def search_caldav(
148105
"""Do the search on a CalDAV calendar.
149106
150107
Only CalDAV-specific parameters goes to this method. Those
151-
parameters are pretty obscure - mostly for power users.
152-
Unless you have some very special needs, the recommendation is
153-
to not use those.
108+
parameters are pretty obscure - mostly for power users and
109+
internal usage. Unless you have some very special needs, the
110+
recommendation is to not use those.
154111
155112
:param calendar: Calendar to be searched
156113
:param server_expand: Ask the CalDAV server to expand recurrences
@@ -159,14 +116,12 @@ def search_caldav(
159116
:param xml: XML query to be sent to the server (string or elements)
160117
:param _hacks: Please don't ask!
161118
119+
Make sure not to confuse he CalDAV properties with iCalendar properties.
120+
162121
If xml is given, any other filtering will not be sent to the server.
163122
They may still be applied through client-side filtering. (TODO: work in progress)
164123
165-
caldav search is the default search as for now - so we have an alias for it ``search``.
166-
167-
``searcher.search(calendar)`` will always work, but no future guarantees are given
168-
that the caldav parameters can be added.
169-
124+
``searcher.search(calendar)`` to apply the search on a caldav server.
170125
"""
171126
if self.expand or server_expand:
172127
if not self.start or not self.end:
@@ -178,7 +133,7 @@ def search_caldav(
178133
if self.start or self.end:
179134
if self._property_filters:
180135
clone = replace(self, _property_filters={})
181-
objects = clone.search_caldav(
136+
objects = clone.search(
182137
calendar, server_expand, split_expanded, props, xml
183138
)
184139
return self.filter(objects)
@@ -237,7 +192,7 @@ def search_caldav(
237192
):
238193
## The algorithm below does not handle recurrence split gently
239194
matches.extend(
240-
clone.search_caldav(
195+
clone.search(
241196
calendar,
242197
server_expand,
243198
split_expanded=False,
@@ -248,7 +203,7 @@ def search_caldav(
248203
)
249204
else:
250205
## The algorithm below does not handle recurrence split gently
251-
matches = clone.search_caldav(
206+
matches = clone.search(
252207
calendar,
253208
server_expand,
254209
split_expanded=False,
@@ -287,7 +242,7 @@ def search_caldav(
287242
if self.include_completed is None:
288243
self.include_completed = True
289244

290-
return self._search_caldav_with_comptypes(
245+
return self._search_with_comptypes(
291246
calendar, server_expand, split_expanded, props, orig_xml, _hacks
292247
)
293248

@@ -304,14 +259,14 @@ def search_caldav(
304259
and not self.comp_class
305260
and not "400" in err.reason
306261
):
307-
return self._search_caldav_with_comptypes(
262+
return self._search_with_comptypes(
308263
calendar, server_expand, split_expanded, props, orig_xml, _hacks
309264
)
310265
raise
311266

312267
## Some things, like `calendar.object_by_uid`, should always work, no matter if `davclient.compatibility_hints` is correctly configured or not
313268
if not objects and not self.comp_class and _hacks == "insist":
314-
return self._search_caldav_with_comptypes(
269+
return self._search_with_comptypes(
315270
calendar, server_expand, split_expanded, props, orig_xml, _hacks
316271
)
317272

@@ -370,8 +325,6 @@ def search_caldav(
370325
self.sort_objects(objects)
371326
return objects
372327

373-
search = search_caldav
374-
375328
def build_search_xml_query(
376329
self, server_expand=False, props=None, filters=None, _hacks=None
377330
):
@@ -515,6 +468,7 @@ def build_search_xml_query(
515468

516469
return (root, self.comp_class)
517470

471+
## TODO: move to base class
518472
def sort_objects(self, objects):
519473
def sort_key_func(x):
520474
ret = []

‎pyproject.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ dependencies = [
3636
"niquests",
3737
"recurring-ical-events>=2.0.0",
3838
"typing_extensions;python_version<'3.11'",
39-
"icalendar>6.0.0"
39+
"icalendar>6.0.0",
40+
#"icalendar-searcher", ## not released at pypi yet
4041
]
4142
dynamic = ["version"]
4243

0 commit comments

Comments
 (0)