2017-12-18 13:49:16 +00:00
|
|
|
"""
|
|
|
|
NTNOE SYNC is a simple script tha allows you to synchronize your NTNOE diary
|
|
|
|
with Google Calendar.
|
|
|
|
|
|
|
|
It will create a `ntnoe` calendar among your Google calendars and write your
|
|
|
|
NTNOE calendar into. NTNOE_SYNC is availabe under the MIT license.
|
|
|
|
|
|
|
|
|
|
|
|
MIT License
|
|
|
|
|
|
|
|
Copyright (c) 2017 Hugo LEVY-FALK
|
|
|
|
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
|
|
in the Software without restriction, including without limitation the rights
|
|
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
|
|
copies or substantial portions of the Software.
|
|
|
|
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
SOFTWARE.
|
|
|
|
"""
|
|
|
|
|
2017-12-04 21:19:04 +00:00
|
|
|
import datetime
|
|
|
|
import httplib2
|
|
|
|
import os
|
2017-12-05 19:05:36 +00:00
|
|
|
import requests
|
2017-12-18 14:09:51 +00:00
|
|
|
import logging
|
|
|
|
from logging.handlers import RotatingFileHandler
|
2017-12-04 21:19:04 +00:00
|
|
|
|
|
|
|
from apiclient import discovery
|
|
|
|
from oauth2client import client
|
|
|
|
from oauth2client import tools
|
|
|
|
from oauth2client.file import Storage
|
|
|
|
|
|
|
|
import icalendar
|
2018-01-30 15:03:50 +00:00
|
|
|
import docopt
|
|
|
|
|
|
|
|
__doc__ = """
|
|
|
|
NTNOE cancer.
|
|
|
|
|
|
|
|
Because NTNOE is cancer, we had to do something to synchronise our Google
|
|
|
|
agenda on it.
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
ntnoe_cancer
|
|
|
|
ntnoe_cancer [--level=<l> --days-future=<df> --days-past=<dp>]
|
|
|
|
ntnoe_cancer -h | --help
|
|
|
|
|
|
|
|
Options:
|
|
|
|
-h --help Show this screen.
|
|
|
|
--level=<l> Level of courses (0=all, 1=skip blue courses (! includes language courses), 2=skip everything but exams, for real man only) [default: 0]
|
|
|
|
--days-future=<df> Number of days in the future to look for the calendar [default: 15]
|
|
|
|
--days-past=<dp> Number of days in the past to look for the calendar, perfect for time travellers [default: 0]
|
|
|
|
"""
|
|
|
|
|
2017-12-04 21:19:04 +00:00
|
|
|
|
2017-12-18 14:09:51 +00:00
|
|
|
DEBUG = False
|
|
|
|
APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
if DEBUG:
|
|
|
|
DATA_DIR = APP_DIR
|
|
|
|
else:
|
|
|
|
home_dir = os.path.expanduser('~')
|
|
|
|
DATA_DIR = os.path.join(home_dir, ".ntnoe", "")
|
|
|
|
if not os.path.exists(DATA_DIR):
|
|
|
|
os.makedirs(DATA_DIR)
|
|
|
|
|
2017-12-18 17:52:43 +00:00
|
|
|
# Logger stuff
|
2017-12-18 14:09:51 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger()
|
|
|
|
|
|
|
|
if DEBUG:
|
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
else:
|
|
|
|
logger.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s')
|
|
|
|
|
|
|
|
file_handler = RotatingFileHandler(
|
|
|
|
os.path.join(DATA_DIR, 'log.txt'), 'a', 1000000, 1)
|
|
|
|
if DEBUG:
|
|
|
|
file_handler.setLevel(logging.DEBUG)
|
|
|
|
else:
|
|
|
|
file_handler.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
file_handler.setFormatter(formatter)
|
|
|
|
logger.addHandler(file_handler)
|
|
|
|
|
|
|
|
if DEBUG:
|
|
|
|
stream_handler = logging.StreamHandler()
|
|
|
|
stream_handler.setLevel(logging.DEBUG)
|
|
|
|
stream_handler.setFormatter(formatter)
|
|
|
|
logger.addHandler(stream_handler)
|
|
|
|
|
2017-12-04 21:19:04 +00:00
|
|
|
|
2017-12-18 13:49:16 +00:00
|
|
|
TIMEDELTA_SYNCHRO = datetime.timedelta(days=15) # Number of days to look for
|
2017-12-04 21:19:04 +00:00
|
|
|
# for synchronization
|
|
|
|
|
2017-12-18 14:29:32 +00:00
|
|
|
with open(os.path.join(APP_DIR, 'ntnoe_credentials')) as f:
|
2017-12-18 13:49:16 +00:00
|
|
|
NTNOE_ID, NTNOE_PASS, _ = f.read().split('\n')
|
2017-12-04 21:19:04 +00:00
|
|
|
|
|
|
|
SCOPES = 'https://www.googleapis.com/auth/calendar'
|
|
|
|
CLIENT_SECRET_FILE = 'client_secret.json'
|
|
|
|
APPLICATION_NAME = 'Google Calendar API Python Quickstart'
|
|
|
|
|
2017-12-18 17:52:43 +00:00
|
|
|
|
2017-12-04 21:19:04 +00:00
|
|
|
class Event:
|
2017-12-18 13:49:16 +00:00
|
|
|
"""
|
|
|
|
The event class allows a simple convertion between `icalendar.cal.Event`
|
|
|
|
and a formatted `dict` for the google API.
|
|
|
|
"""
|
|
|
|
|
2017-12-04 21:19:04 +00:00
|
|
|
# ColorId corresponding to course code
|
2018-01-30 15:03:50 +00:00
|
|
|
class Course:
|
|
|
|
AMPHI = '9'
|
|
|
|
TL = '11'
|
|
|
|
TD = '10'
|
|
|
|
AUTRE = '13'
|
|
|
|
EXAM = '12'
|
|
|
|
|
|
|
|
SKILL_LEVEL = {
|
|
|
|
'0' : {Course.AMPHI, Course.TL, Course.TD, Course.AUTRE, Course.EXAM},
|
|
|
|
'1' : {Course.TL, Course.TD, Course.AUTRE, Course.EXAM},
|
|
|
|
'2' : {Course.EXAM},
|
|
|
|
}
|
2017-12-04 21:19:04 +00:00
|
|
|
|
2018-01-30 15:03:50 +00:00
|
|
|
EVENT_COLOR = {
|
|
|
|
Course.AMPHI: '9',
|
|
|
|
Course.TL: '10',
|
|
|
|
Course.TD: '6',
|
|
|
|
Course.AUTRE: '5',
|
|
|
|
Course.EXAM: '3',
|
2017-12-04 21:19:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, e):
|
|
|
|
""" Initialize an event from a `icalendar.cal.Event`."""
|
|
|
|
self.summary = e.decoded('SUMMARY').decode('utf-8')
|
|
|
|
self.start = e.decoded('DTSTART')
|
|
|
|
self.end = e.decoded('DTEND')
|
|
|
|
self.location = e.decoded('LOCATION').decode('utf-8')
|
2018-01-30 15:03:50 +00:00
|
|
|
self.type = e.decoded('DESCRIPTION').decode('utf-8')
|
|
|
|
self.colorid = self.EVENT_COLOR.get(self.type, '1')
|
2017-12-04 21:19:04 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return str(self.as_google())
|
|
|
|
|
|
|
|
def as_google(self):
|
2017-12-18 13:49:16 +00:00
|
|
|
"""Returns the event as a formatted `dict` for the google API."""
|
2017-12-04 21:19:04 +00:00
|
|
|
return {
|
2017-12-18 13:49:16 +00:00
|
|
|
'summary': self.summary,
|
|
|
|
'location': self.location,
|
|
|
|
'start': {
|
|
|
|
'dateTime': self.start.isoformat(),
|
|
|
|
'timeZone': 'Europe/Paris',
|
2017-12-04 21:19:04 +00:00
|
|
|
},
|
2017-12-18 13:49:16 +00:00
|
|
|
'end': {
|
|
|
|
'dateTime': self.end.isoformat(),
|
|
|
|
'timeZone': 'Europe/Paris',
|
2017-12-04 21:19:04 +00:00
|
|
|
},
|
2017-12-18 13:49:16 +00:00
|
|
|
'colorId': self.colorid,
|
2017-12-04 21:19:04 +00:00
|
|
|
'reminders': {
|
|
|
|
'useDefault': False,
|
|
|
|
'overrides': [],
|
|
|
|
},
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def get_credentials():
|
|
|
|
"""Gets valid user credentials from storage.
|
|
|
|
|
|
|
|
If nothing has been stored, or if the stored credentials are invalid,
|
|
|
|
the OAuth2 flow is completed to obtain the new credentials.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Credentials, the obtained credential.
|
|
|
|
"""
|
2017-12-18 14:09:51 +00:00
|
|
|
credential_dir = os.path.join(DATA_DIR, 'credentials')
|
2017-12-04 21:19:04 +00:00
|
|
|
if not os.path.exists(credential_dir):
|
|
|
|
os.makedirs(credential_dir)
|
|
|
|
credential_path = os.path.join(credential_dir, 'ntnoe.json')
|
|
|
|
|
|
|
|
store = Storage(credential_path)
|
|
|
|
credentials = store.get()
|
|
|
|
if not credentials or credentials.invalid:
|
|
|
|
flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
|
|
|
|
flow.user_agent = APPLICATION_NAME
|
2017-12-18 14:09:51 +00:00
|
|
|
logger.info('Storing credentials to ' + credential_path)
|
2017-12-04 21:19:04 +00:00
|
|
|
credentials = tools.run_flow(flow, store)
|
|
|
|
|
|
|
|
return credentials
|
|
|
|
|
|
|
|
|
|
|
|
def get_ntnoe():
|
2017-12-18 13:49:16 +00:00
|
|
|
"""Retrieves the calendar on NTNOE."""
|
|
|
|
r = requests.post(
|
2017-12-04 21:19:04 +00:00
|
|
|
"https://ntnoe.metz.supelec.fr/ical/index.php",
|
2017-12-18 17:52:43 +00:00
|
|
|
data={"envoyer": "Utf8_All", "submit": "G%E9n%E9rer"},
|
2017-12-05 19:05:36 +00:00
|
|
|
auth=(NTNOE_ID, NTNOE_PASS),
|
2017-12-04 21:19:04 +00:00
|
|
|
)
|
|
|
|
|
2017-12-18 13:49:16 +00:00
|
|
|
url = "https://ntnoe.metz.supelec.fr/ical/EdTcustom/Eleves/edt_{}.ics"
|
|
|
|
url = url.format(NTNOE_ID)
|
|
|
|
r = requests.get(url, auth=(NTNOE_ID, NTNOE_PASS))
|
|
|
|
return r.content
|
2017-12-04 21:19:04 +00:00
|
|
|
|
2017-12-18 17:52:43 +00:00
|
|
|
|
2018-01-30 15:03:50 +00:00
|
|
|
def main(arguments):
|
2017-12-04 21:19:04 +00:00
|
|
|
"""Get the events on NTNOE the puts them on Google Calendar.
|
|
|
|
"""
|
2017-12-18 14:14:13 +00:00
|
|
|
|
|
|
|
# Authentication on google
|
|
|
|
logger.info('Authenticating on Google')
|
2017-12-04 21:19:04 +00:00
|
|
|
credentials = get_credentials()
|
|
|
|
http = credentials.authorize(httplib2.Http())
|
|
|
|
service = discovery.build('calendar', 'v3', http=http)
|
|
|
|
|
2017-12-18 14:14:13 +00:00
|
|
|
# Retrieving the calendar on NTNOE
|
|
|
|
logger.info('Requesting NTNOE for {}'.format(NTNOE_ID))
|
2017-12-18 13:49:16 +00:00
|
|
|
ical = icalendar.Calendar.from_ical(get_ntnoe())
|
|
|
|
|
2017-12-18 14:14:13 +00:00
|
|
|
# We need to find the id of the calendar we will edit.
|
|
|
|
logger.info('Looking for `ntnoe` calendar.')
|
2017-12-18 13:49:16 +00:00
|
|
|
calendars = service.calendarList().list().execute()
|
|
|
|
ntnoe_calendar_id = None
|
|
|
|
for c in calendars['items']:
|
|
|
|
if c['summary'] == 'ntnoe':
|
|
|
|
ntnoe_calendar_id = c['id']
|
|
|
|
|
|
|
|
if not ntnoe_calendar_id:
|
2017-12-18 14:14:13 +00:00
|
|
|
logger.info("Creating `ntnoe` calendar...")
|
2017-12-18 13:49:16 +00:00
|
|
|
created = service.calendars().insert(body={
|
2017-12-18 17:52:43 +00:00
|
|
|
'defaultReminders': [],
|
|
|
|
'selected': True,
|
|
|
|
'summary': 'ntnoe',
|
2017-12-18 13:49:16 +00:00
|
|
|
}).execute()
|
|
|
|
ntnoe_calendar_id = created['id']
|
2017-12-04 21:19:04 +00:00
|
|
|
|
|
|
|
now = datetime.datetime.now()
|
2018-01-30 15:03:50 +00:00
|
|
|
then = now + datetime.timedelta(days=int(arguments["--days-future"]))
|
|
|
|
before = now - datetime.timedelta(days=int(arguments["--days-past"]))
|
|
|
|
logger.info('Looking for events between {} and {}.'.format(before, then))
|
2017-12-18 13:49:16 +00:00
|
|
|
|
2017-12-18 14:14:13 +00:00
|
|
|
# NTNOE calendar often changes. So let's delete former synchronizations.
|
|
|
|
logger.info('Deleting former events.')
|
2017-12-18 13:49:16 +00:00
|
|
|
former_ones = service.events().list(
|
|
|
|
calendarId=ntnoe_calendar_id,
|
2017-12-04 21:19:04 +00:00
|
|
|
).execute()
|
|
|
|
|
2017-12-18 13:49:16 +00:00
|
|
|
for event in former_ones['items']:
|
2017-12-18 14:15:43 +00:00
|
|
|
logger.debug('Deleting event : {}'.format(event['id']))
|
2017-12-18 13:49:16 +00:00
|
|
|
service.events().delete(
|
|
|
|
calendarId=ntnoe_calendar_id,
|
|
|
|
eventId=event['id']
|
|
|
|
).execute()
|
|
|
|
|
2018-01-30 15:03:50 +00:00
|
|
|
l = arguments['--level']
|
|
|
|
logger.info('Adding new events with skill={}.'.format(l))
|
|
|
|
granted_events = Event.SKILL_LEVEL[l]
|
2017-12-18 13:49:16 +00:00
|
|
|
for e in ical.walk('VEVENT'):
|
2017-12-04 21:19:04 +00:00
|
|
|
event = Event(e)
|
2018-01-30 15:03:50 +00:00
|
|
|
if before >= event.end or event.start >= then:
|
2017-12-04 21:19:04 +00:00
|
|
|
continue
|
|
|
|
|
2018-01-30 15:03:50 +00:00
|
|
|
if event.type in granted_events:
|
|
|
|
event = service.events().insert(
|
|
|
|
calendarId=ntnoe_calendar_id,
|
|
|
|
body=event.as_google()
|
|
|
|
).execute()
|
|
|
|
logger.debug("Adding event : {}".format(event['id']))
|
2017-12-18 13:49:16 +00:00
|
|
|
|
2017-12-04 21:19:04 +00:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2018-01-30 15:03:50 +00:00
|
|
|
arguments = docopt.docopt(__doc__, version='NTNOE Cancer 1.0')
|
|
|
|
main(arguments)
|