My calendar has been pretty packed recently. I’m covering an extra territory, interviewing people for positions, doing internal training, and a bunch of other things. With all that I have had some times when I’ve been septuple booked. That’s seven meetings at once. When they are all important how do you pick which one to go to? This seems to be a question I’ve always gotten in interview but that’s beside the point. This time I couldn’t pick the “right” one. They were all for things that needed to get done that day and at that time.
So what do you do? Well you know me. Automate everything. So I whipped up a flask app to give me a calendar roulette wheel!

The wheel loads meetings from your calendar and puts the subjects on the wheel. By clicking go the wheel spins for a random time between 1 and 30 seconds until it lands on one meeting. Attend that one!

Most of the code comes from the Microsoft azure-samples repo: https://github.com/Azure-Samples/ms-identity-python-flask-webapp-authentication. All of the Azure AD app registration prerequisites apply. They are detailed in that repo. I added one new route to app.py:
@app.route('/roulette')
@ms_identity_web.login_required
def roulette():
ms_identity_web.acquire_token_silently()
graph = app.config['GRAPH_ENDPOINT']
token = f'Bearer {ms_identity_web.id_data._access_token}'
meetings = find_meetings(graph, token)
return render_template('roulette.html', results=meetings)
The find meetings function comes from my graph helpers.
import urllib
import webbrowser
import requests
from datetime import date, datetime, timezone
import json
def api_endpoint(url):
"""Convert a relative path such as /me/photo/$value to a full URI based
on the current RESOURCE and API_VERSION settings in config.py.
"""
RESOURCE = 'https://graph.microsoft.com'
API_VERSION = 'beta'
if urllib.parse.urlparse(url).scheme in ['http', 'https']:
return url # url is already complete
return urllib.parse.urljoin(f'{RESOURCE}/{API_VERSION}/',
url.lstrip('/'))
def find_meetings(session, token, startDate = date.today(), endDate = date.today(),**kwargs):
startDateTime = datetime.combine(startDate,datetime.min.time()).astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
endDateTime = datetime.combine(endDate,datetime.max.time()).astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
endpoint = f'me/calendarview?startDateTime={startDateTime}&endDateTime={endDateTime}'
response = requests.get(api_endpoint(endpoint), headers={'Content-Type': 'application/json','Authorization': token})
values = response.json()['value']
try:
next = response.json()['@odata.nextLink']
except:
next = None
while next is not None:
response = requests.get(next,headers={'Content-Type': 'application/json'})
try:
values = values + response.json()['value']
except:
next = None
try:
next = response.json()['@odata.nextLink']
except:
next = None
return values
The api_endpoint function is from the azure-samples device flow example with a slight modification. Since there is only graph call this could just be part of the find_meetings function but copy and paste is easier than rewriting code. This is all saved in helpers.py and imported into app.py:
from helpers import find_meetings
The bulk of the code for this one is roulette.html. Lots of javascript to draw the canvas do do the rotations.
<!doctype html>
<html>
<head>
<script>
var myInterval;
var rotInt = 0;
function spin() {
rotInt = rotInt+.99;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.globalCompositeOperation = 'destination-over';
ctx.save();
clearCanvas();
ctx.translate(300,300);
ctx.rotate((Math.PI *.1)*rotInt);
ctx.translate(-300,-300);
draw()
ctx.restore();
ctx.moveTo(300,600);
ctx.fillStyle="black";
ctx.lineTo(300,550);
ctx.stroke();
}
function draw() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
clearCanvas()
var subjects = [];
{% for result in results %}
{% if 'subject' in result %}
subjects.push("{{ result.subject }}");
{% endif %}
{% endfor %}
var colors = ['#4CAF50', '#00BCD4', '#E91E63', '#FFC107', '#9E9E9E', '#CDDC39'];
if(parseInt(document.getElementById("slices").value) > 0){
var sliceAngle = parseFloat(2/parseInt(document.getElementById("slices").value))
var angles = []
for(var i = 0; i < parseInt(document.getElementById("slices").value); i++){
num = parseFloat(Math.PI * sliceAngle)
angles.push(num)
}
}else{
var angles = [Math.PI * 0.3, Math.PI * 0.7, Math.PI * 0.2, Math.PI * 0.4, Math.PI * 0.4];
}
var beginAngle = 0;
var endAngle = 0;
for(var i = 0; i < angles.length; i = i + 1) {
beginAngle = endAngle;
endAngle = endAngle + angles[i];
ctx.beginPath();
ctx.fillStyle = colors[i % colors.length];
ctx.moveTo(300, 300);
ctx.arc(300, 300, 220, beginAngle, endAngle);
ctx.lineTo(300, 300);
ctx.stroke();
ctx.fill();
}
beginAngle = 0;
endAngle = 0;
for(var i = 0; i < angles.length; i = i + 1) {
beginAngle = endAngle;
endAngle = endAngle + angles[i];
var pieRadius = Math.min(ctx.canvas.width / 2, ctx.canvas.height / 2);
var labelX = 300 + (pieRadius / 2) * Math.cos(beginAngle + (endAngle - beginAngle) / 2);
var labelY = 300 + (pieRadius / 2) * Math.sin(beginAngle + (endAngle - beginAngle) / 2)
ctx.fillStyle = "black";
ctx.font = "bold 10px Arial";
ctx.fillText(subjects[i], labelX, labelY);
}
}
function clearCanvas() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
//rotInt = 0;
}
window.onload = draw;
</script>
</head>
<body>
<canvas id="canvas" width="600" height="600"></canvas>
<form>
{% set num_results = results|length %}
<input type="number" name="slices" id="slices" value="{{ num_results }}"">
<input type="button" value="Redraw" onclick="draw()">
<input type="button" value="spin" onclick="myInterval = setInterval(spin,100);">
<input type="button" value="stop" onclick="clearInterval(myInterval);">
<input type="button" value="Random Stop (30 Seconds)" onclick="setTimeout(function(){ clearInterval(myInterval); },Math.random()*1000*30);">
</form>
</body>
</html>
This file goes in the templates directory of the app. Once there you go to the main page of the app https://127.0.0.1:5000 is the default debug location when running it. Once you login just go to https://127.0.0.1:5000/roulette and the app will pull your invites for the next hour, put them on the wheel, click GO and they spin until one is picked. No more thinking. It would be even funnier with sound. No github again secrets, tenant ids, and app ids….