-
Notifications
You must be signed in to change notification settings - Fork 98
/
Copy pathbasic_usage_examples.py
348 lines (282 loc) · 12.4 KB
/
basic_usage_examples.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
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()