Skip to content

Commit fb5da3e

Browse files
authored
Config file, examples and misc
Various unrelated work got a bit mixed up here: * Work on finding and parsing a config file, including some test code and documentation. Fixes #485 * Brushing up the examples. Fixing test code for some examples. Adding more information on the examples in the documentation. Ref Road Map at #474 * Old test code utilizing vobjects has been rewritten from `obj.instance` to `obj.vobject_instance` to reduce the number of deprecation warnings when running tests #507
1 parent 8c34103 commit fb5da3e

13 files changed

Lines changed: 213 additions & 64 deletions

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,25 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0
1010

1111
### Deprecated
1212

13-
* The `event.instance` property currently yields a vobject. For quite many years people have asked for the python vobject library to be replaced with the python icalendar objects, but I haven't been able to do that due to backward compatibility. In version 2.0 deprecation warnings will be given whenever someone uses the `event.instance` property. In 3.0, perhaps `event.instance` will yield a `icalendar` instance.
13+
* The `event.instance` property currently yields a vobject. For quite many years people have asked for the python vobject library to be replaced with the python icalendar objects, but I haven't been able to do that due to backward compatibility. In version 2.0 deprecation warnings will be given whenever someone uses the `event.instance` property. In 3.0, perhaps `event.instance` will yield a `icalendar` instance. Old test code has been updated to use `.vobject_instance` instead of `.instance`.
1414
* `calendar.date_search` - use `calendar.search` instead. (this one has been deprecated for a while, but only with info-logging)
1515
* `davclient.auto_conn` that was introduced just some days ago has already been renamed to `davclient.get_davclient`.
1616

1717
### Added
1818

1919
* `event.component` is now an alias for `event.icalendar_component`.
20-
* `get_davclient` (earlier called `auto_conn`) is more complete now - it could already read from test config, now it can read from environment (including environment variable for reading from test config and for locating the config file). While the `auto_conn` itself is tested in the functional tests, the code for reading the config file (and all the corner cases) is not tested. It's allowable with a yaml config file, but the yaml module is not included in the dependencies yet ... so late imports as for now. - https://github.com/python-caldav/caldav/pull/502 - https://github.com/python-caldav/caldav/issues/485
20+
* `get_davclient` (earlier called `auto_conn`) is more complete now - https://github.com/python-caldav/caldav/pull/502 - https://github.com/python-caldav/caldav/issues/485 - https://github.com/python-caldav/caldav/pull/507
21+
* It can read from environment (including environment variable for reading from test config and for locating the config file).
22+
* It can read from a config file. New parameter `check_config_file`, defaults to true
23+
* It will probe default locations for the config file (`~/.config/caldav/calendar.conf`, `~/.config/caldav/calendar.yaml`, `~/.config/caldav/calendar.json`, `~/.config/calendar.conf`, `/etc/calendar.conf`, `/etc/caldav/calendar.conf` as for now)
24+
* Improved tests (but no test for inheritance yet).
25+
* Documentation, linked up from the reference section of the doc.
26+
* It's allowable with a yaml config file, but the yaml module is not included in the dependencies yet ... so late imports as for now, and the import is wrapped in a try/except-block
27+
* Looked through and brushed up the examples, two of them are now executed by the unit tests. Added a doc section on the examples.
28+
29+
### Fixes
30+
31+
* Support for Lark/Feishu got broken in the 1.6-release. Issue found and fixed by Hongbin Yang (github user @zealseeker) in https://github.com/python-caldav/caldav/issues/505 and https://github.com/python-caldav/caldav/pull/506
2132

2233
### Changed
2334

caldav/config.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import json
2+
import logging
3+
import os
24

35
"""
46
This configuration parsing code was just copied from my plann library (and will be removed from there at some point in the future). It's lacking tests, documentation and ... generally just lacking.
@@ -71,23 +73,42 @@ def config_section(config, section="default"):
7173

7274

7375
def read_config(fn, interactive_error=False):
76+
if not fn:
77+
cfgdir = f"{os.environ.get('HOME', '/')}/.config/"
78+
for config_file in (
79+
f"{cfgdir}/caldav/calendar.conf",
80+
f"{cfgdir}/caldav/calendar.yaml"
81+
f"{cfgdir}/caldav/calendar.json"
82+
f"{cfgdir}/calendar.conf",
83+
"/etc/calendar.conf",
84+
"/etc/caldav/calendar.conf",
85+
):
86+
cfg = read_config(config_file)
87+
if cfg:
88+
return cfg
89+
return None
90+
7491
## This can probably be refactored into fewer lines ...
7592
try:
7693
try:
7794
with open(fn, "rb") as config_file:
7895
return json.load(config_file)
7996
except json.decoder.JSONDecodeError:
80-
## Late import. yaml is external module,
97+
## Late import, wrapped in try/except. yaml is external module,
8198
## and not included in the requirements as for now.
82-
## TODO: should wrap it in try: ... except: log readable error
83-
import yaml
84-
8599
try:
86-
with open(fn, "rb") as config_file:
87-
return yaml.load(config_file, yaml.Loader)
88-
except yaml.scanner.ScannerError:
100+
import yaml
101+
102+
try:
103+
with open(fn, "rb") as config_file:
104+
return yaml.load(config_file, yaml.Loader)
105+
except yaml.scanner.ScannerError:
106+
logging.error(
107+
f"config file {fn} exists but is neither valid json nor yaml. Check the syntax."
108+
)
109+
except ImportError:
89110
logging.error(
90-
"config file exists but is neither valid json nor yaml. Check the syntax."
111+
f"config file {fn} exists but is not valid json, and pyyaml is not installed."
91112
)
92113

93114
except FileNotFoundError:

caldav/davclient.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ def request(
888888

889889

890890
def auto_calendars(
891-
config_file: str = f"{os.environ.get('HOME')}/.config/calendar.conf",
891+
config_file: str = None,
892892
config_section="default",
893893
testconfig=False,
894894
environment: bool = True,
@@ -928,9 +928,10 @@ def auto_conn(*largs, config_data: dict = None, **kwargs):
928928

929929

930930
def get_davclient(
931-
config_file: str = f"{os.environ.get('HOME')}/.config/calendar.conf",
932-
config_section="default",
933-
testconfig=False,
931+
check_config_file: bool = True,
932+
config_file: str = None,
933+
config_section: str = None,
934+
testconfig: bool = False,
934935
environment: bool = True,
935936
name: str = None,
936937
**config_data,
@@ -977,16 +978,26 @@ def get_davclient(
977978

978979
if environment:
979980
conf = {}
980-
for conf_key in (x for x in os.environ if x.startswith("CALDAV_")):
981+
for conf_key in (
982+
x
983+
for x in os.environ
984+
if x.startswith("CALDAV_") and not x.startswith("CALDAV_CONFIG")
985+
):
981986
conf[conf_key[7:].lower()] = os.environ[conf_key]
982987
if conf:
983988
return DAVClient(**conf)
984-
config_file = os.environ.get("CALDAV_CONFIG_FILE")
989+
if not config_file:
990+
config_file = os.environ.get("CALDAV_CONFIG_FILE")
991+
if not config_section:
992+
config_section = os.enviorn.get("CALDAV_CONFIG_SECTION")
985993

986-
if config_file:
994+
if check_config_file:
987995
## late import in 2.0, as the config stuff isn't properly tested
988996
from . import config
989997

998+
if not config_section:
999+
config_section = "default"
1000+
9901001
cfg = config.read_config(config_file)
9911002
if cfg:
9921003
section = config.config_section(cfg, config_section)

docs/source/configfile.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
==================
2+
Config file format
3+
==================
4+
5+
6+
The :class:`davclient.get_davclient` method (and perhaps in 2.1, also ``davclient.get_calendar``) can read from a config file. It will look for it in the following locations:
7+
8+
* ``$HOME/.config/caldav/calendar.conf``
9+
* ``$HOME/.config/caldav/calendar.yaml``
10+
* ``$HOME/.config/caldav/calendar.json``
11+
* ``$HOME/.config/calendar.conf``
12+
* ``/etc/calendar.conf``
13+
14+
The config file has to be valid json or yaml (support for toml and Apple pkl may be considered).
15+
16+
The config file is expected to be divided in sections, where each section can describe locations and credentials to a CalDAV server, a CalDAV calendar or a collection of calendars/servers. As of version 2.0, only the first is supported.
17+
18+
A config section can be given either through parameters to :class:`davclient.get_davclient` or by enviornment variable ``CALDAV_CONFIG_SECTION``. If no section is given, the ``default`` section is used.
19+
20+
Connection parameters
21+
=====================
22+
23+
The section should contain configuration keys and values. All configuration keys starting with ``caldav_`` is considered to be connection parameters and is passed to the DAVClient object. Typically, ``caldav_url``, ``caldav_username`` and ``caldav_password`` should be passed.
24+
25+
Calendar parameters
26+
===================
27+
28+
Not implemented yet.
29+
30+
Probably in version 2.1 or version 2.2, ``calendar_name``, ``calendar_id`` and ``calendar_url`` can be used to specify a calendar.
31+
32+
Inheritance and collections
33+
===========================
34+
35+
A section may ``inherit`` another section. This may typically be used if having several sections in the config file corresponding to the same server/user but different calendars, or several sections corresponding to the same calendar server, but different users.
36+
37+
If a section ``contains`` different other sections, it's efficiently a collection of calendars. This is not relevant for 2.0 though.
38+
39+
Simple example
40+
==============
41+
42+
.. code-block: yaml
43+
44+
---
45+
default:
46+
caldav_url: http://caldav.example.com/dav/
47+
caldav_user: tor
48+
caldav_pass: hunter2

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Contents
1515
about
1616
tutorial
1717
reference
18+
examples
1819

1920
====================
2021
Indices and tables

docs/source/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Contents
1010
.. toctree::
1111
:maxdepth: 1
1212

13+
configfile.md
1314
caldav/davclient
1415
caldav/davobject
1516
caldav/collection

docs/source/tutorial.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ Tutorial
55
In this tutorial you should learn basic usage of the python CalDAV
66
client library. You are encouraged to copy the code examples into a
77
file and add a ``breakpoint()`` inside the with-block so you can
8-
inspect the return objects you get from the library calls.
8+
inspect the return objects you get from the library calls. Do not
9+
name your file `caldav.py` or `calendar.py`, this may break some
10+
imports.
911

1012
To follow this tutorial as intended, each code block should be run
1113
towards a clean-slate Radicale server. To do this, you need:

examples/basic_usage_examples.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
sys.path.insert(0, ".")
99

1010
import caldav
11+
from caldav.davclient import get_davclient
1112

1213
## DO NOT name your file calendar.py or caldav.py! We've had several
1314
## issues filed, things break because the wrong files are imported.
@@ -29,13 +30,10 @@ def run_examples():
2930
## The client object stores http session information, username, password, etc.
3031
## As of 1.0, Initiating the client object will not cause any server communication,
3132
## so the credentials aren't validated.
33+
## get_davclient will try to read credentials and url from environment variables
34+
## and config file.
3235
## The client object can be used as a context manager, like this:
33-
with caldav.DAVClient(
34-
url=caldav_url,
35-
username=username,
36-
password=password,
37-
headers=headers, # Optional parameter to set HTTP headers on each request if needed
38-
) as client:
36+
with get_davclient() as client:
3937
## Typically the next step is to fetch a principal object.
4038
## This will cause communication with the server.
4139
my_principal = client.principal()
@@ -132,20 +130,20 @@ def read_modify_event_demo(event):
132130
## event.icalendar_instance gives an icalendar instance - which
133131
## normally would be one icalendar calendar object containing one
134132
## subcomponent. Quite often the fourth property,
135-
## icalendar_component is preferable - it gives us the component -
136-
## but be aware that if the server returns a recurring events with
137-
## exceptions, event.icalendar_component will ignore all the
138-
## exceptions.
139-
uid = event.icalendar_component["uid"]
133+
## icalendar_component (now available just as .component) is
134+
## preferable - it gives us the component - but be aware that if
135+
## the server returns a recurring events with exceptions,
136+
## event.icalendar_component will ignore all the exceptions.
137+
uid = event.component["uid"]
140138

141139
## Let's correct that typo using the icalendar library.
142-
event.icalendar_component["summary"] = event.icalendar_component["summary"].replace(
140+
event.component["summary"] = event.component["summary"].replace(
143141
"celebratiuns", "celebrations"
144142
)
145143

146144
## timestamps (DTSTAMP, DTSTART, DTEND for events, DUE for tasks,
147145
## etc) can be fetched using the icalendar library like this:
148-
dtstart = event.icalendar_component.get("dtstart")
146+
dtstart = event.component.get("dtstart")
149147

150148
## but, dtstart is not a python datetime - it's a vDatetime from
151149
## the icalendar package. If you want it as a python datetime,
@@ -156,13 +154,13 @@ def read_modify_event_demo(event):
156154

157155
## We can modify it:
158156
if dtstart:
159-
event.icalendar_component["dtstart"].dt = dtstart.dt + timedelta(seconds=3600)
157+
event.component["dtstart"].dt = dtstart.dt + timedelta(seconds=3600)
160158

161159
## And finally, get the casing correct
162160
event.data = event.data.replace("norwegian", "Norwegian")
163161

164162
## Note that this is not quite thread-safe:
165-
icalendar_component = event.icalendar_component
163+
icalendar_component = event.component
166164
## accessing the data (and setting it) will "disconnect" the
167165
## icalendar_component from the event
168166
event.data = event.data
@@ -183,10 +181,7 @@ def read_modify_event_demo(event):
183181
## Finally, let's verify that the correct data was saved
184182
calendar = event.parent
185183
same_event = calendar.event_by_uid(uid)
186-
assert (
187-
same_event.icalendar_component["summary"]
188-
== "Norwegian national day celebrations"
189-
)
184+
assert same_event.component["summary"] == "Norwegian national day celebrations"
190185

191186

192187
def search_calendar_demo(calendar):

examples/get_events_example.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
from caldav.davclient import get_davclient
55

6-
## Code contributed by Крылов Александр. Minor changes and quite some
7-
## comments by Tobias Brox.
6+
## Code contributed by Крылов Александр.
7+
## Minor changes by Tobias Brox.
8+
## All comments by Tobias Brox.
89
## Set CALDAV_USERNAME, CALDAV_URL and CALDAV_PASSWORD through
910
## environment variables before running this example
1011

@@ -37,6 +38,9 @@ def fill_event(component, calendar) -> dict[str, str]:
3738
cur["calendar"] = f"{calendar}"
3839
cur["summary"] = component.get("summary")
3940
cur["description"] = component.get("description")
41+
## month/day/year time? Never ever do that!
42+
## It's one of the most confusing date formats ever!
43+
## Use year-month-day time instead ... https://xkcd.com/1179/
4044
cur["start"] = component.start.strftime("%m/%d/%Y %H:%M")
4145
endDate = component.end
4246
if endDate:

examples/scheduling_examples.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## NOTE! This is currently NOT tested. It may and may not work.
2+
## Please reach out if you need help with scheduling ...
3+
## use https://github.com/python-caldav/caldav/issues
4+
## or scheduling-help@plann.no
15
import sys
26
import uuid
37
from datetime import datetime

0 commit comments

Comments
 (0)