Skip to content

Commit a8c9575

Browse files
chore: working prototype
1 parent 5427b54 commit a8c9575

File tree

5 files changed

+153
-16
lines changed

5 files changed

+153
-16
lines changed

ical-simplejson-proxy/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
## Configuration
44

5-
### Environment Variables
5+
### HTTP Headers
66
#### Required
7-
- `ICAL_URL` - The URL to the iCal feed or file. Examples: `file://path/to/file.ics`,
8-
`https://my.calendar.org/feeds/calendar.ics`
7+
- `X-ICAL-URL` - The URL of the iCal feed.
8+
#### Optional
9+
- `X-TAGS` - A comma delimited list of tags for the annotations. Example: `maintenance,IT,planned`
10+
11+
### Environment Variables
912
#### Optional
10-
- `CACHE_TTL` - Default: 1800 - The number of seconds that results should be cached for.
13+
- `CACHE_TTL` - Default: 1800 - The number of seconds that results should be cached for.

ical-simplejson-proxy/app.py

Lines changed: 118 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,126 @@
1-
from flask import Flask
1+
import os
2+
import requests
3+
import time
4+
import icalendar
5+
6+
from datetime import datetime
7+
from flask import Flask, request, abort
8+
from flask_cors import CORS
9+
from requests_file import FileAdapter
10+
from cachetools import TTLCache, cached
11+
12+
CACHE_TTL = os.environ.get("CACHE_TTL", 1800)
13+
14+
CACHE = TTLCache(maxsize=1000, ttl=CACHE_TTL)
215

316
app = Flask(__name__)
417

5-
_SERVER_NAME = 'iCal SimpleJson Proxy'
18+
cors = CORS(app)
19+
app.config["CORS_HEADERS"] = "Content-Type"
20+
21+
methods = ["GET", "POST", "HEAD", "OPTIONS"]
22+
623

724
@app.route("/")
8-
def ok():
25+
def index():
926
return "OK"
1027

11-
@app.route("/query")
28+
29+
@app.route("/search", methods=methods)
30+
def search():
31+
return []
32+
33+
34+
@app.route("/query", methods=methods)
1235
def query():
13-
return {}
14-
15-
@app.after_request
16-
def add_cors_headers(response):
17-
response.headers['Access-Control-Allow-Headers'] = 'accept, content-type'
18-
response.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
19-
response.headers['Access-Control-Allow-Origin'] = '*'
20-
return response
36+
return []
37+
38+
39+
@app.route("/annotations", methods=methods)
40+
def annotations():
41+
if request.method == "POST":
42+
annotation = request.json.get(
43+
"annotation",
44+
{
45+
"name": "Test Annotation",
46+
"datasource": {
47+
"type": "grafana-simple-json-datasource",
48+
"uid": "TEST",
49+
},
50+
"enable": True,
51+
"iconColor": "red",
52+
"query": "#test",
53+
},
54+
)
55+
ical_url = request.headers.get(
56+
"X-ICAL-URL",
57+
f"file://{os.path.dirname(os.path.realpath(__file__))}/fixtures/calendar.ics",
58+
)
59+
60+
tags = []
61+
if "X-TAGS" in request.headers:
62+
tags = request.headers.get("X-TAGS").split(",")
63+
64+
return _ical_annotations(ical_url, tags, annotation)
65+
return []
66+
67+
68+
@app.route("/tag-keys", methods=methods)
69+
def tag_keys():
70+
return []
71+
72+
73+
@app.route("/tag-values", methods=methods)
74+
def tag_values():
75+
return []
76+
77+
78+
def _ical_annotations_cache_key(url: str, tags: list, *args):
79+
return f'{url}/{",".join(tags)}'
80+
81+
82+
@cached(CACHE, _ical_annotations_cache_key)
83+
def _ical_annotations(url: str, tags: list, annotation: dict):
84+
ical_data = _fetch_ical_data(url)
85+
ical_as_annotations = _convert_ical_to_annotations(ical_data, tags)
86+
87+
return [dict(x, **{"annotation": annotation}) for x in ical_as_annotations]
88+
89+
90+
def _fetch_ical_data(url: str):
91+
s = requests.Session()
92+
s.mount("file://", FileAdapter())
93+
94+
ical_resp = s.get(url)
95+
if ical_resp.status_code != 200:
96+
abort(ical_resp.status_code)
97+
98+
ical_data = ical_resp.text
99+
if not ical_data.startswith("BEGIN:VCALENDAR"):
100+
abort(400)
101+
102+
return ical_data
103+
104+
105+
def _convert_ical_to_annotations(ical_data, tags):
106+
ical_annotations = []
107+
ical_calendar = icalendar.Calendar.from_ical(ical_data)
108+
109+
for component in ical_calendar.walk():
110+
if component.name == "VEVENT":
111+
try:
112+
ical_annotations.append(
113+
{
114+
"time": int(
115+
time.mktime(component.get("dtstart").dt.timetuple()) * 1000
116+
),
117+
"title": component.get("summary"),
118+
"tags": tags,
119+
"text": component.get("description", ""),
120+
}
121+
)
122+
except AttributeError:
123+
print(
124+
f'level=ERROR msg="decoding event failed" event_summary="{component.get("summary")}" event_dtstart="{component.get("dtstart")}" event_dtstamp="{component.get("dtstamp")}" event_dtend="{component.get("dtend")}"'
125+
)
126+
return ical_annotations
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
BEGIN:VCALENDAR
2+
PRODID:-//Google Inc//Google Calendar 70.9054//EN
3+
VERSION:2.0
4+
CALSCALE:GREGORIAN
5+
METHOD:PUBLISH
6+
X-WR-CALNAME:Scheduled Maintenance
7+
X-WR-TIMEZONE:Europe/London
8+
X-WR-CALDESC:A calendar for IT/Platform/Other maintenance events
9+
BEGIN:VEVENT
10+
DTSTART:20220916T113000Z
11+
DTEND:20220916T123000Z
12+
DTSTAMP:20220921T132404Z
13+
UID:0sgn0d9cqakfvm7rj6a8niq2re@google.com
14+
CREATED:20220916T093411Z
15+
DESCRIPTION:Something is happening at this time
16+
LAST-MODIFIED:20220916T093411Z
17+
LOCATION:
18+
SEQUENCE:0
19+
STATUS:CONFIRMED
20+
SUMMARY:Test event
21+
TRANSP:TRANSPARENT
22+
END:VEVENT
23+
END:VCALENDAR
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"annotation":{"datasource":{"type":"grafana-simple-json-datasource","uid":"0x5XTb7Vk"},"enable":true,"iconColor":"red","name":"Default Annotation","query":"#test"},"tags":[],"text":"Something is happening at this time","time":1663327800000,"title":"Test event"}]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
Flask==2.2.2
2+
Flask-Cors==3.0.10
3+
requests==2.28.1
4+
requests-file==1.5.1
5+
ics==0.7.2

0 commit comments

Comments
 (0)