PythonでNextCloud(CalDAV)のカレンダーの予定を取得する


# 概要
初めての記事投稿になります。

この記事ではPythonでNextCloudのカレンダーの予定を取得する方法を載せようと思います。
自分が試そうと思ったときに日本語の記事が見つからなかったため、ここに残しておこうと思います。
動機としては研究室のカレンダーがNextCloudを使用しており、それを手軽に見れるようにしたいなといった感じです。
ThunderBirdやIOSのカレンダーにCalDAVをインポートすれば表示されるのですが、据え置きのディスプレイを見るだけで今日の予定が知ることができればもっといいなと思いました。


# 使用するモジュール
今回使用するPythonモジュールはcaldavとdatetimeです。

caldavをインストールしていない方はまずインストールをしましょう。
```
pip install caldav
```

caldavのドキュメントは[こちら](https://caldav.readthedocs.io/en/latest/)を参照。

次にPythonでインポートしましょう。
```python
import caldav
import datetime
```

# NextCloudからカレンダーの情報を取得する
まずクライアント側からカレンダーから色々情報を取得するコードを書きます。
まずCalDAVのURLを取得しましょう。
NextCloudの場合、設定とインポートから取得できると思います。
今回はPythonから取得したいので、**通常のCalDAVアドレスをコピー**を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2805504/c466fbfc-5a7b-7035-eb9a-dbdd250c2742.png)

取得したURLを使ってPythonクライアントからカレンダーの情報を取得します。
urlには先ほどのcalDAVアドレスを入れてください。usernameとpasswordは認証がある場合は入力してください。
自分の研究室のNextCloudでは、認証があったためそちらの値を使用しました。
認証がない場合は省略だと思います。

```python
client = caldav.DAVClient(url=url, username=username, password=password)
# プリンシパル オブジェクトの設定
principal = client.principal()

# カレンダーの取得
calendars = principal.calendars()
```
これでカレンダーの情報を入手できたので、ここから**予定**を抽出していきます。

まず```calendars```ですがここにはNextCloudにあるすべてのカレンダーがリストとして入っています。
リスト内のカレンダーをそのまま```print```すると個別のカレンダーのURLが表示されます。また```calendar.name```を```print```するとカレンダーの名前が出てきます。 
```python
for calendar in calendars:
    # カレンダーのURLを表示
    print(calendar)

    # カレンダーの名前を表示
    print(calendar.name)
```
他にも```calendar.add```でカレンダーに予定を追加したりすることができますが、今回は予定の抽出のみを行います。

# カレンダーから予定を取得する
次にカレンダーから予定を取得していきます。
カレンダーの取得には日付を指定した方が良いです。日付を指定しないとすべての予定が取得されてしまいます。

日付は予定の始まりと終わりを指定し、datetime型で入力します。
今回は今日の予定を取得します。

今日の予定は以下のようになっています。
カレンダーは2種類ありtest1、test2は同一のカレンダー、test3はもう1つのカレンダーの予定となっております。
test1とtest2では終日かそうでないかで違っています。ここの違いが後にデータの形の差として現れてきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2805504/6636023f-092c-88ff-f669-ff14abbb1c3a.png)

そしてPython今日の予定を取得したい場合は以下のように書きます。
```python
# 今日の日付を取得
today = datetime.datetime.now()


for calendar in calendars:
    try:
        events = calendar.date_search(start=today, end=today+datetime.timedelta(days=1), expand=True)
    except:
        continue
```
```today```には今日の日付を入れています。```today+datetime.timedelta(days=1)```には明日の日付(今日の日付+1日)が入っています。
エラー処理を入れないとエラーを吐くので入れています。(おそらく指定した期間で予定がない場合にエラーを吐くのかも...)
エラー処理の書き方が良くないのは見逃してください。

```events```には拡張子が```.ics```のファイルのURLが入ったEvent型のデータが入っています。```.ics```については各自で調べてください。

まだここでは予定が見れていない状態なので、この```events```から予定を確認できる形までもっていきます。
```python
for event in events:
    print(event.data)
```
このevent.dataにはstr型の文字列が入っています。ここにはカレンダーに入力されている予定の詳細が1つの文字列として入っています。データの区切りは```\r\n```で表現されています。

出力結果 (UIDとPRODIDは隠しています)
```python
'BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:*//EN\r\nBEGIN:VEVENT\r\nUID:*\r\nDTSTART;VALUE=DATE:20220823\r\nDTEND;VALUE=DATE:20220824\r\nCREATED:20220822T223116Z\r\nDTSTAMP:20220822T223125Z\r\nLAST-MODIFIED:20220822T223125Z\r\nSEQUENCE:2\r\nSTATUS:CONFIRMED\r\nSUMMARY:test\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n'
```
もちろんこのままでは見にくいので見やすい形に持っていきましょう。
やり方はなんでもいいですが、今回は```splitlines()```を使用しました。
```python
event.splitlines()

# 出力
['BEGIN:VCALENDAR',
 'VERSION:2.0',
 'CALSCALE:GREGORIAN',
 'PRODID:*',
 'BEGIN:VEVENT',
 'UID:*',
 'DTSTART;VALUE=DATE:20220823',
 'DTEND;VALUE=DATE:20220824',
 'CREATED:20220822T223116Z',
 'DTSTAMP:20220822T223125Z',
 'LAST-MODIFIED:20220822T223125Z',
 'SEQUENCE:3',
 'STATUS:CONFIRMED',
 'SUMMARY:test2',
 'END:VEVENT',
 'END:VCALENDAR']
```
見やすいですね。また辞書に入れたほうが扱いやすそうだったのでこれを辞書に変換しました。
```python
d = {}
for event_data in event.data.splitlines():
    pair = event_data.split(":")
    d[pair[0]] = pair[1]
print(d)

# 出力
{'BEGIN': 'VEVENT', 'VERSION': '2.0', 'CALSCALE': 'GREGORIAN', 'PRODID': '*', 'UID': '*', 'DTSTART;VALUE=DATE': '20220823', 'DTEND;VALUE=DATE': '20220824', 'CREATED': '20220822T223116Z', 'DTSTAMP': '20220822T224939Z', 'LAST-MODIFIED': '20220822T224939Z', 'SEQUENCE': '3', 'STATUS': 'CONFIRMED', 'SUMMARY': 'test2', 'END': 'VCALENDAR'}
```

ここで出力されている```'SUMMARY'```が予定の名前になります。
そして予定の始まりと終わりについてですが、場合によって```key```の部分の値が変わってきます。
今回の3つのテストの出力は以下のようになります。

```
{'BEGIN': 'VEVENT',
  'VERSION': '2.0',
  'CALSCALE': 'GREGORIAN',
  'PRODID': '*',
  'UID': '*',
  'DTSTART;VALUE=DATE': '20220823',
  'DTEND;VALUE=DATE': '20220824',
  'CREATED': '20220822T223100Z',
  'DTSTAMP': '20220822T224941Z',
  'LAST-MODIFIED': '20220822T224941Z',
  'SEQUENCE': '3',
  'STATUS': 'CONFIRMED',
  'SUMMARY': 'test3',
  'END': 'VCALENDAR'},
 {'BEGIN': 'VEVENT',
  'VERSION': '2.0',
  'CALSCALE': 'GREGORIAN',
  'PRODID': '*',
  'UID': '*',
  'DTSTART': '20220823T010000Z',
  'DTEND': '20220823T020000Z',
  'CREATED': '20220822T223132Z',
  'DTSTAMP': '20220822T224936Z',
  'LAST-MODIFIED': '20220822T224936Z',
  'SEQUENCE': '3',
  'STATUS': 'CONFIRMED',
  'SUMMARY': 'test1',
  'END': 'VCALENDAR'},
 {'BEGIN': 'VEVENT',
  'VERSION': '2.0',
  'CALSCALE': 'GREGORIAN',
  'PRODID': '*',
  'UID': '*',
  'DTSTART;VALUE=DATE': '20220823',
  'DTEND;VALUE=DATE': '20220824',
  'CREATED': '20220822T223116Z',
  'DTSTAMP': '20220822T224939Z',
  'LAST-MODIFIED': '20220822T224939Z',
  'SEQUENCE': '3',
  'STATUS': 'CONFIRMED',
  'SUMMARY': 'test2',
  'END': 'VCALENDAR'}
```
出力結果からわかる通り'DTSTART'だったり'DTSTART;VALUE=DATE'だったりします。先ほど少し触れましたがこれは終日が終日でないかの違いになります。終日でない（つまり時間指定）場合、予定の始まりは'DTSTART'、予定の終わりは'DTEND'となっています。
逆に終日の場合'DTSTART;VALUE=DATE'が予定の始まりの日、'DTEND;VALUE=DATE'が予定の終わりになります。
今回は記述していないので省略されていますが、予定の説明は```DESCRIPTION```になります。他の値については調べるなり考えてみるなりお願いします。


そして最後にこれを元にデータを表示します。各自好きなデータを使いましょう。
一例として予定の名前とその始まりと終わりをまとめるコードを書きました。参考程度にしてください。
```python
if 'SUMMARY' in schedule.keys():
    key = schedule['SUMMARY']
if 'DTSTART' in d.keys():
    start = d['DTSTART'][:8]
elif 'DTSTART;VALUE=DATE' in d.keys():
    start = a['DTSTART;VALUE=DATE'][:8]
    
if 'DTEND' in d.keys():
    end = d['DTSTART'][:8]
elif 'DTEND;VALUE=DATE' in d.keys():
    end = d['DTEND;VALUE=DATE'][:8]

print(key)
print((start,end))

# 出力
'test1'
('20220823', '20220823')
```

といった感じになります。```calendar.name```もつけてあげるともっとわかりやすいかもしれませんね。


# 完成版コード
最後に今回のコードをまとめたものを書いておきます。上のやつと少し違う部分がありますがそこは読み解いてください。
```python
import caldav
import datetime

url = ''
username = ''
password = ''

client = caldav.DAVClient(url=url, username=username, password=password)
principal = client.principal()
calendars = principal.calendars()
    
today = datetime.datetime.now()
schedules = []
for calendar in calendars:
        try:
            events = calendar.date_search(start=today, end=today+datetime.timedelta(days=1), expand=True)
            for event in events:
                d = {}
                for event_data in event.data.splitlines():
                    pair = event_data.split(":")
                    d[pair[0]] = pair[1]
                schedules.append(d)
        except:
            continue

d = {}
for schedule in schedules:
    if 'SUMMARY' in schedule.keys():
        key = schedule['SUMMARY']
    if 'DTSTART' in schedule.keys():
        value1 = schedule['DTSTART'][:8]
    elif 'DTSTART;VALUE=DATE' in schedule.keys():
        value1 = schedule['DTSTART;VALUE=DATE'][:8]
    
    if 'DTEND' in schedule.keys():
        value2 = schedule['DTSTART'][:8]
    elif 'DTEND;VALUE=DATE' in schedule.keys():
        value2 = schedule['DTEND;VALUE=DATE'][:8]
    d[key] = (value1,value2)
print(d)
```