import sys from datetime import date from datetime import datetime from datetime import timedelta ## We'll try to use the local caldav library, not the system-installed sys.path.insert(0, "..") sys.path.insert(0, ".") import caldav ## DO NOT name your file calendar.py or caldav.py! We've had several ## issues filed, things break because the wrong files are imported. ## It's not a bug with the caldav library per se. ## CONFIGURATION. Edit here, or set up something in ## tests/conf_private.py (see tests/conf_private.py.EXAMPLE). caldav_url = "https://calendar.example.com/dav" username = "somebody" password = "hunter2" headers = {"X-MY-CUSTOMER-HEADER": "123"} def run_examples(): """ Run through all the examples, one by one """ ## We need a client object. ## The client object stores http session information, username, password, etc. ## As of 1.0, Initiating the client object will not cause any server communication, ## so the credentials aren't validated. ## The client object can be used as a context manager, like this: with caldav.DAVClient( url=caldav_url, username=username, password=password, headers=headers, # Optional parameter to set HTTP headers on each request if needed ) as client: ## Typically the next step is to fetch a principal object. ## This will cause communication with the server. my_principal = client.principal() ## The principals calendars can be fetched like this: calendars = my_principal.calendars() ## print out some information print_calendars_demo(calendars) ## This cleans up from previous runs, if needed: find_delete_calendar_demo(my_principal, "Test calendar from caldav examples") ## Let's create a new calendar to play with. ## This may raise an error for multiple reasons: ## * server may not support it (it's not mandatory in the CalDAV RFC) ## * principal may not have the permission to create calendars ## * some cloud providers have a global namespace my_new_calendar = my_principal.make_calendar( name="Test calendar from caldav examples" ) ## Let's add some events to our newly created calendar add_stuff_to_calendar_demo(my_new_calendar) ## Let's find the stuff we just added to the calendar event = search_calendar_demo(my_new_calendar) ## Inspecting and modifying an event read_modify_event_demo(event) ## Accessing a calendar by a calendar URL calendar_by_url_demo(client, my_new_calendar.url) ## Clean up - delete things ## (The event would normally be deleted together with the calendar, ## but different calendar servers may behave differently ...) event.delete() my_new_calendar.delete() def calendar_by_url_demo(client, url): """Sometimes one may have a calendar URL. Sometimes maybe one would not want to fetch the principal object from the server (it's not even required to support it by the caldav protocol). """ ## No network traffic will be initiated by this: calendar = client.calendar(url=url) ## At the other hand, this will cause network activity: events = calendar.events() ## We should still have only one event in the calendar assert len(events) == 1 event_url = events[0].url ## there is no similar method for fetching an event through ## a URL. One may construct the object like this though: same_event = caldav.Event(client=client, parent=calendar, url=event_url) ## That was also done without any network traffic. To get the same_event ## populated with data it needs to be loaded: same_event.load() assert same_event.data == events[0].data def read_modify_event_demo(event): """This demonstrates how to edit properties in the ical object and save it back to the calendar. It takes an event - caldav.Event - as input. This event is found through the `search_calendar_demo`. The event needs some editing, which will be done below. Keep in mind that the differences between an Event, a Todo and a Journal is small, everything that is done to he event here could as well be done towards a task. """ ## The objects (events, journals and tasks) comes with some properties that ## can be used for inspecting the data and modifying it. ## event.data is the raw data, as a string, with unix linebreaks print("here comes some icalendar data:") print(event.data) ## event.wire_data is the raw data as a byte string with CRLN linebreaks assert len(event.wire_data) >= len(event.data) ## Two libraries exists to handle icalendar data - vobject and ## icalendar. The caldav library traditionally supported the ## first one, but icalendar is more popular. ## Here is an example ## on how to modify the summary using vobject: event.vobject_instance.vevent.summary.value = "norwegian national day celebratiuns" ## event.icalendar_instance gives an icalendar instance - which ## normally would be one icalendar calendar object containing one ## subcomponent. Quite often the fourth property, ## icalendar_component is preferable - it gives us the component - ## but be aware that if the server returns a recurring events with ## exceptions, event.icalendar_component will ignore all the ## exceptions. uid = event.icalendar_component["uid"] ## Let's correct that typo using the icalendar library. event.icalendar_component["summary"] = event.icalendar_component["summary"].replace( "celebratiuns", "celebrations" ) ## timestamps (DTSTAMP, DTSTART, DTEND for events, DUE for tasks, ## etc) can be fetched using the icalendar library like this: dtstart = event.icalendar_component.get("dtstart") ## but, dtstart is not a python datetime - it's a vDatetime from ## the icalendar package. If you want it as a python datetime, ## use the .dt property. (In this case dtstart is set - and it's ## pretty much mandatory for events - but the code here is robust ## enough to handle cases where it's undefined): dtstart_dt = dtstart and dtstart.dt ## We can modify it: if dtstart: event.icalendar_component["dtstart"].dt = dtstart.dt + timedelta(seconds=3600) ## And finally, get the casing correct event.data = event.data.replace("norwegian", "Norwegian") ## Note that this is not quite thread-safe: icalendar_component = event.icalendar_component ## accessing the data (and setting it) will "disconnect" the ## icalendar_component from the event event.data = event.data ## So this will not affect the event anymore: icalendar_component["summary"] = "do the needful" assert not "do the needful" in event.data ## The mofifications are still only saved locally in memory - ## let's save it to the server: event.save() ## NOTE: always use event.save() for updating events and ## calendar.save_event(data) for creating a new event. ## This may break: # event.save(event.data) ## ref https://github.com/python-caldav/caldav/issues/153 ## Finally, let's verify that the correct data was saved calendar = event.parent same_event = calendar.event_by_uid(uid) assert ( same_event.icalendar_component["summary"] == "Norwegian national day celebrations" ) def search_calendar_demo(calendar): """ some examples on how to fetch objects from the calendar """ ## It should theoretically be possible to find both the events and ## tasks in one calendar query, but not all server implementations ## supports it, hence either event, todo or journal should be set ## to True when searching. Here is a date search for events, with ## expand: events_fetched = calendar.search( start=datetime.now(), end=datetime(date.today().year + 5, 1, 1), event=True, expand=True, ) ## "expand" causes the recurrences to be expanded. ## The yearly event will give us one object for each year assert len(events_fetched) > 1 print("here is some ical data:") print(events_fetched[0].data) ## We can also do the same thing without expand, then the "master" ## from 2020 will be fetched events_fetched = calendar.search( start=datetime.now(), end=datetime(date.today().year + 5, 1, 1), event=True, expand=False, ) assert len(events_fetched) == 1 ## search can be done by other things, i.e. keyword tasks_fetched = calendar.search(todo=True, category="outdoor") assert len(tasks_fetched) == 1 ## This those should also work: all_objects = calendar.objects() # updated_objects = calendar.objects_by_sync_token(some_sync_token) # some_object = calendar.object_by_uid(some_uid) # some_event = calendar.event_by_uid(some_uid) children = calendar.children() events = calendar.events() tasks = calendar.todos() assert len(events) + len(tasks) == len(all_objects) assert len(children) == len(all_objects) ## TODO: Some of those should probably be deprecated. ## children is a good candidate. ## Tasks can be completed tasks[0].complete() ## They will then disappear from the task list assert not calendar.todos() ## But they are not deleted assert len(calendar.todos(include_completed=True)) == 1 ## Let's delete it completely tasks[0].delete() return events_fetched[0] def print_calendars_demo(calendars): """ This example prints the name and URL for every calendar on the list """ if calendars: ## Some calendar servers will include all calendars you have ## access to in this list, and not only the calendars owned by ## this principal. print("your principal has %i calendars:" % len(calendars)) for c in calendars: print(" Name: %-36s URL: %s" % (c.name, c.url)) else: print("your principal has no calendars") def find_delete_calendar_demo(my_principal, calendar_name): """ This example takes a calendar name, finds the calendar if it exists, and deletes the calendar if it exists. """ ## Let's try to find or create a calendar ... try: ## This will raise a NotFoundError if calendar does not exist demo_calendar = my_principal.calendar(name="Test calendar from caldav examples") assert demo_calendar print( f"We found an existing calendar with name {calendar_name}, now deleting it" ) demo_calendar.delete() except caldav.error.NotFoundError: ## Calendar was not found pass def add_stuff_to_calendar_demo(calendar): """ This demo adds some stuff to the calendar Unfortunately the arguments that it's possible to pass to save_* is poorly documented. https://github.com/python-caldav/caldav/issues/253 """ ## Add an event with some certain attributes may_event = calendar.save_event( dtstart=datetime(2020, 5, 17, 6), dtend=datetime(2020, 5, 18, 1), summary="Do the needful", rrule={"FREQ": "YEARLY"}, ) ## not all calendars supports tasks ... but if it's supported, it should be ## told here: acceptable_component_types = calendar.get_supported_components() assert "VTODO" in acceptable_component_types ## Add a task that should contain some ical lines ## Note that this may break on your server: ## * not all servers accepts tasks and events mixed on the same calendar. ## * not all servers accepts tasks at all dec_task = calendar.save_todo( ical_fragment="""DTSTART;VALUE=DATE:20201213 DUE;VALUE=DATE:20201220 SUMMARY:Chop down a tree and drag it into the living room RRULE:FREQ=YEARLY PRIORITY: 2 CATEGORIES: outdoor""" ) ## ical_fragment parameter -> just some lines ## ical parameter -> full ical object def _please_ignore_this_hack(): """ This hack is to be used for the maintainer (or other people having set up testing servers in tests/private_conf.py) to be able to verify that this example code works, without editing the example code itself. """ if password == "hunter2": from tests.conf import client as client_ client = client_() def _wrapper(*args, **kwargs): return client caldav.DAVClient = _wrapper if __name__ == "__main__": _please_ignore_this_hack() run_examples()