1010import logging
1111import uuid
1212
13- import icalendar
1413from niquests import AsyncSession
1514
1615from caldav .jmap ._methods .calendar import build_calendar_get , parse_calendar_get
1716from caldav .jmap ._methods .event import (
1817 build_event_changes ,
1918 build_event_get ,
20- build_event_query ,
2119 build_event_set_destroy ,
2220 build_event_set_update ,
2321 parse_event_changes ,
3331 parse_task_set ,
3432)
3533from caldav .jmap .client import _DEFAULT_USING , _TASK_USING , _JMAPClientBase
36- from caldav .jmap .convert import ical_to_jscal , jscal_to_ical
34+ from caldav .jmap .convert import ical_to_jscal
3735from caldav .jmap .error import JMAPAuthError , JMAPMethodError
3836from caldav .jmap .objects .calendar import JMAPCalendar
37+ from caldav .jmap .objects .calendar_object import JMAPCalendarObject
3938from caldav .jmap .session import Session , async_fetch_session
4039
4140log = logging .getLogger ("caldav.jmap" )
@@ -188,14 +187,17 @@ async def create_event(self, calendar_id: str, ical_str: str) -> str:
188187
189188 raise JMAPMethodError (url = session .api_url , reason = "No CalendarEvent/set response" )
190189
191- async def get_event (self , event_id : str ) -> str :
190+ async def get_event (self , event_id : str ) -> JMAPCalendarObject :
192191 """Fetch a calendar event as an iCalendar string.
193192
194193 Args:
195194 event_id: The JMAP event ID to retrieve.
196195
197196 Returns:
198- A VCALENDAR string for the event.
197+ A :class:`~caldav.jmap.objects.calendar_object.JMAPCalendarObject`
198+ wrapping the raw JSCalendar dict. ``parent`` is ``None`` since
199+ no :class:`~caldav.jmap.objects.calendar.JMAPCalendar` is available
200+ at the client level.
199201
200202 Raises:
201203 JMAPMethodError: If the event is not found.
@@ -213,7 +215,7 @@ async def get_event(self, event_id: str) -> str:
213215 reason = f"Event not found: { event_id } " ,
214216 error_type = "notFound" ,
215217 )
216- return jscal_to_ical ( items [0 ])
218+ return JMAPCalendarObject ( data = items [0 ], parent = None )
217219
218220 raise JMAPMethodError (url = session .api_url , reason = "No CalendarEvent/get response" )
219221
@@ -248,36 +250,18 @@ async def _search(
248250 start : str | None = None ,
249251 end : str | None = None ,
250252 text : str | None = None ,
251- ) -> list [str ]:
253+ parent : JMAPCalendar | None = None ,
254+ ) -> list [JMAPCalendarObject ]:
252255 session = await self ._get_session ()
253- filter_dict : dict = {}
254- if calendar_id is not None :
255- filter_dict ["inCalendars" ] = [calendar_id ]
256- if start is not None :
257- filter_dict ["after" ] = start
258- if end is not None :
259- filter_dict ["before" ] = end
260- if text is not None :
261- filter_dict ["text" ] = text
262-
263- query_call = build_event_query (session .account_id , filter = filter_dict or None )
264- get_call = (
265- "CalendarEvent/get" ,
266- {
267- "accountId" : session .account_id ,
268- "#ids" : {
269- "resultOf" : "ev-query-0" ,
270- "name" : "CalendarEvent/query" ,
271- "path" : "/ids" ,
272- },
273- },
274- "ev-get-1" ,
275- )
276- responses = await self ._request ([query_call , get_call ])
256+ calls = self ._build_event_search_calls (session .account_id , calendar_id , start , end , text )
257+ responses = await self ._request (calls )
277258
278259 for method_name , resp_args , _ in responses :
279260 if method_name == "CalendarEvent/get" :
280- return [jscal_to_ical (item ) for item in resp_args .get ("list" , [])]
261+ return [
262+ JMAPCalendarObject (data = item , parent = parent )
263+ for item in resp_args .get ("list" , [])
264+ ]
281265
282266 return []
283267
@@ -287,8 +271,8 @@ async def search_events(
287271 start : str | None = None ,
288272 end : str | None = None ,
289273 text : str | None = None ,
290- ) -> list [str ]:
291- """Search for calendar events and return them as iCalendar strings .
274+ ) -> list [JMAPCalendarObject ]:
275+ """Search for calendar events.
292276
293277 All parameters are optional; omitting all returns every event in the account.
294278 Results are fetched in a single batched JMAP request using a result reference
@@ -301,7 +285,9 @@ async def search_events(
301285 text: Free-text search across title, description, locations, and participants.
302286
303287 Returns:
304- List of VCALENDAR strings for all matching events.
288+ List of :class:`~caldav.jmap.objects.calendar_object.JMAPCalendarObject`
289+ instances. ``parent`` is ``None`` on these objects; use
290+ :meth:`JMAPCalendar.search` if you need ``parent`` set.
305291 """
306292 return await self ._search (calendar_id = calendar_id , start = start , end = end , text = text )
307293
@@ -325,13 +311,14 @@ async def get_sync_token(self) -> str:
325311
326312 async def get_objects_by_sync_token (
327313 self , sync_token : str
328- ) -> tuple [list [str ], list [str ], list [str ]]:
314+ ) -> tuple [list [JMAPCalendarObject ], list [JMAPCalendarObject ], list [str ]]:
329315 """Fetch events changed since a previous sync token.
330316
331317 Calls ``CalendarEvent/changes`` to discover which events were created,
332318 modified, or destroyed since ``sync_token`` was issued. Created and
333- modified events are returned as iCalendar strings; destroyed events are
334- returned as IDs (the objects no longer exist on the server).
319+ modified events are returned as
320+ :class:`~caldav.jmap.objects.calendar_object.JMAPCalendarObject` instances;
321+ destroyed events are returned as IDs (the objects no longer exist on the server).
335322
336323 Args:
337324 sync_token: A state string previously returned by :meth:`get_sync_token`
@@ -340,8 +327,8 @@ async def get_objects_by_sync_token(
340327 Returns:
341328 A 3-tuple ``(added, modified, deleted)``:
342329
343- - ``added``: iCalendar strings for newly created events.
344- - ``modified``: iCalendar strings for updated events.
330+ - ``added``: objects for newly created events (``parent`` is ``None``) .
331+ - ``modified``: objects for updated events (``parent`` is ``None``) .
345332 - ``deleted``: Event IDs that were destroyed.
346333
347334 Raises:
@@ -376,11 +363,11 @@ async def get_objects_by_sync_token(
376363 get_call = build_event_get (session .account_id , ids = fetch_ids )
377364 get_responses = await self ._request ([get_call ])
378365
379- events_by_id : dict [str , str ] = {}
366+ events_by_id : dict [str , JMAPCalendarObject ] = {}
380367 for method_name , resp_args , _ in get_responses :
381368 if method_name == "CalendarEvent/get" :
382369 for item in resp_args .get ("list" , []):
383- events_by_id [item ["id" ]] = jscal_to_ical ( item )
370+ events_by_id [item ["id" ]] = JMAPCalendarObject ( data = item , parent = None )
384371
385372 added = [events_by_id [i ] for i in created_ids if i in events_by_id ]
386373 modified = [events_by_id [i ] for i in updated_ids if i in events_by_id ]
@@ -408,19 +395,12 @@ async def delete_event(self, event_id: str) -> None:
408395
409396 raise JMAPMethodError (url = session .api_url , reason = "No CalendarEvent/set response" )
410397
411- async def _get_object_by_uid (self , uid : str , calendar_id : str | None = None ) -> str :
412- for event_ical in await self ._search (calendar_id = calendar_id ):
413- try :
414- cal = icalendar .Calendar .from_ical (event_ical )
415- for component in cal .walk ():
416- if component .name in ("VEVENT" , "VTODO" , "VJOURNAL" ):
417- component_uid = component .get ("UID" )
418- if component_uid is not None and str (component_uid ) == uid :
419- return event_ical
420- except ValueError :
421- log .debug ("Skipping unparseable iCalendar string during UID lookup" )
422- continue
423-
398+ async def _get_object_by_uid (
399+ self , uid : str , calendar_id : str | None = None , parent : JMAPCalendar | None = None
400+ ) -> JMAPCalendarObject :
401+ for obj in await self ._search (calendar_id = calendar_id , parent = parent ):
402+ if obj .data .get ("uid" ) == uid :
403+ return obj
424404 session = await self ._get_session ()
425405 raise JMAPMethodError (
426406 url = session .api_url , reason = f"No calendar object found with UID: { uid } "
0 commit comments