@@ -228,6 +228,7 @@ def _build_ical(
228228 description : str = "" ,
229229 location : str = "" ,
230230 status : str = "CONFIRMED" ,
231+ categories : list [str ] | None = None ,
231232) -> str :
232233 """Build a minimal iCalendar VEVENT string."""
233234 cal = ICal ()
@@ -245,6 +246,8 @@ def _build_ical(
245246 event .add ("location" , location )
246247 if status :
247248 event .add ("status" , status )
249+ if categories :
250+ event .add ("categories" , categories )
248251 cal .add_component (event )
249252 return cal .to_ical ().decode ()
250253
@@ -257,6 +260,13 @@ def _validate_status(status: str) -> str:
257260 return upper
258261
259262
263+ def _set_prop (component : Any , name : str , value : Any , clear_if_empty : bool = False ) -> None :
264+ component .pop (name , None )
265+ if clear_if_empty and not value :
266+ return
267+ component .add (name .lower (), value )
268+
269+
260270def _apply_event_updates (
261271 component : Any ,
262272 summary : str | None ,
@@ -265,30 +275,24 @@ def _apply_event_updates(
265275 description : str | None ,
266276 location : str | None ,
267277 status : str | None ,
278+ categories : list [str ] | None = None ,
268279) -> None :
269280 if summary is not None :
270- component .pop ("SUMMARY" , None )
271- component .add ("summary" , summary )
281+ _set_prop (component , "SUMMARY" , summary )
272282 if start is not None :
273- component .pop ("DTSTART" , None )
274- component .add ("dtstart" , _parse_dt (start , _is_all_day (component .get ("DTSTART" ))))
283+ _set_prop (component , "DTSTART" , _parse_dt (start , _is_all_day (component .get ("DTSTART" ))))
275284 if end is not None :
276285 ref = component .get ("DTEND" ) or component .get ("DTSTART" )
277- component .pop ("DTEND" , None )
278- component .add ("dtend" , _parse_dt (end , _is_all_day (ref )))
286+ _set_prop (component , "DTEND" , _parse_dt (end , _is_all_day (ref )))
279287 if description is not None :
280- component .pop ("DESCRIPTION" , None )
281- if description :
282- component .add ("description" , description )
288+ _set_prop (component , "DESCRIPTION" , description , clear_if_empty = True )
283289 if location is not None :
284- component .pop ("LOCATION" , None )
285- if location :
286- component .add ("location" , location )
290+ _set_prop (component , "LOCATION" , location , clear_if_empty = True )
287291 if status is not None :
288- component . pop ( "STATUS" , None )
289- component . add ( "status" , status )
290- component . pop ( "DTSTAMP " , None )
291- component . add ( "dtstamp " , datetime .now (UTC ))
292+ _set_prop ( component , "STATUS" , status )
293+ if categories is not None :
294+ _set_prop ( component , "CATEGORIES " , categories , clear_if_empty = True )
295+ _set_prop ( component , "DTSTAMP " , datetime .now (UTC ))
292296
293297
294298async def _find_event (calendar_id : str , event_uid : str ) -> tuple [str , str , str ]:
@@ -419,6 +423,7 @@ async def create_event(
419423 description : str = "" ,
420424 location : str = "" ,
421425 status : str = "CONFIRMED" ,
426+ categories : str = "" ,
422427 ) -> str :
423428 """Create a new calendar event.
424429
@@ -434,11 +439,13 @@ async def create_event(
434439 description: Optional event description/notes.
435440 location: Optional event location.
436441 status: Event status: "CONFIRMED" (default), "TENTATIVE", or "CANCELLED".
442+ categories: Optional comma-separated category names (e.g. "Work,Meeting").
437443
438444 Returns:
439445 JSON object with the created event's uid and summary.
440446 """
441447 status_upper = _validate_status (status )
448+ cat_list = [c .strip () for c in categories .split ("," ) if c .strip ()] if categories else None
442449 dtstart = _parse_dt (start , all_day )
443450 if end :
444451 dtend = _parse_dt (end , all_day )
@@ -448,7 +455,7 @@ async def create_event(
448455 dtend = dtstart + timedelta (hours = 1 )
449456
450457 uid = str (uuid .uuid4 ())
451- ical_data = _build_ical (uid , summary , dtstart , dtend , description , location , status_upper )
458+ ical_data = _build_ical (uid , summary , dtstart , dtend , description , location , status_upper , cat_list )
452459 client = get_client ()
453460 user = get_config ().user
454461 path = _caldav_path (user , calendar_id , f"{ uid } .ics" )
@@ -474,6 +481,7 @@ async def update_event(
474481 description : str | None = None ,
475482 location : str | None = None ,
476483 status : str | None = None ,
484+ categories : str | None = None ,
477485 ) -> str :
478486 """Update an existing calendar event. Only provided fields are changed.
479487
@@ -489,16 +497,20 @@ async def update_event(
489497 description: New description. Pass "" to clear.
490498 location: New location. Pass "" to clear.
491499 status: New status: "CONFIRMED", "TENTATIVE", or "CANCELLED".
500+ categories: New categories as comma-separated string. Pass "" to clear.
492501
493502 Returns:
494503 Confirmation message with the updated event UID.
495504 """
496505 validated_status = _validate_status (status ) if status is not None else None
506+ cat_list : list [str ] | None = None
507+ if categories is not None :
508+ cat_list = [c .strip () for c in categories .split ("," ) if c .strip ()] if categories else []
497509 href , etag , ical_data = await _find_event (calendar_id , event_uid )
498510 cal = ICal .from_ical (ical_data )
499511 for component in cal .walk ():
500512 if component .name == "VEVENT" :
501- _apply_event_updates (component , summary , start , end , description , location , validated_status )
513+ _apply_event_updates (component , summary , start , end , description , location , validated_status , cat_list )
502514 break
503515
504516 client = get_client ()
0 commit comments