77from typing import List
88from typing import Optional
99
10+ from icalendar_searcher import Searcher
1011from icalendar .prop import TypesFactory
1112from lxml import etree
1213
2324TypesFactory = 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 = []
0 commit comments