Skip to content

Instantly share code, notes, and snippets.

@afunTW
Last active March 7, 2024 18:18
Show Gist options
  • Save afunTW/bf958eea15835d14aa976990d1f0bb88 to your computer and use it in GitHub Desktop.
Save afunTW/bf958eea15835d14aa976990d1f0bb88 to your computer and use it in GitHub Desktop.
sample code for icalendar and google calendar API

Intro

Thers's some MS-defined column in .ics that will be ignored when import .ics to google calendar. We can self modified those value and insert/update the google calendar by google calendar API.

  • icalendar: parse the .ics file
  • google-api-python-client: google calendar API

Usage

usage: main.py [-h] [--auth_host_name AUTH_HOST_NAME]
               [--noauth_local_webserver]
               [--auth_host_port [AUTH_HOST_PORT [AUTH_HOST_PORT ...]]]
               [--logging_level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] --ics ICS
               [--cal CAL]

Outlook .ics file to gcal by gcal API

optional arguments:
  -h, --help            show this help message and exit
  --auth_host_name AUTH_HOST_NAME
                        Hostname when running a local web server.
  --noauth_local_webserver
                        Do not run a local web server.
  --auth_host_port [AUTH_HOST_PORT [AUTH_HOST_PORT ...]]
                        Port web server should listen on.
  --logging_level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
                        Set the logging level of detail.
  --ics ICS             input file
  --cal CAL             execute to which calendar

Reference

You can check the google calendar API Python sample first, then create your own code by following document

import argparse
import datetime
import os
import re
import httplib2
from apiclient import discovery
from bs4 import BeautifulSoup
from icalendar import Calendar, Event, vText
from oauth2client import client, tools
from oauth2client.file import Storage
from apiclient.http import BatchHttpRequest
from pprint import pprint
# If modifying these scopes, delete your previously saved credentials
# at ~/.credentials/calendar-python.json
SCOPES = 'https://www.googleapis.com/auth/calendar'
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'Google Calendar API Python'
def argparser():
parser = argparse.ArgumentParser(
parents=[tools.argparser],
description='Outlook .ics file to gcal by gcal API'
)
parser.add_argument('--ics', dest='ics', required=True, help='input file')
parser.add_argument('--cal', dest='cal', default='primary', help='execute to which calendar')
return parser
def get_credentials(flags):
"""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.
"""
home_dir = os.path.expanduser('~')
credential_dir = os.path.join(home_dir, '.credentials')
if not os.path.exists(credential_dir):
os.makedirs(credential_dir)
credential_path = os.path.join(credential_dir, 'calendar-python.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
if flags:
credentials = tools.run_flow(flow, store, flags)
else: # Needed only for compatibility with Python 2.6
credentials = tools.run(flow, store)
print('Storing credentials to ' + credential_path)
return credentials
def get_calendarId(service, summary):
page_token = None
while True:
calendar_list = service.calendarList().list(pageToken=page_token).execute()
for calendar_list_entry in calendar_list['items']:
if calendar_list_entry['summary'] == summary:
return calendar_list_entry['id']
page_token = calendar_list.get('nextPageToken')
if not page_token:
break
def parse_ics(ics):
events = []
with open(ics, 'r') as rf:
ical = Calendar().from_ical(rf.read())
ical_config = dict(ical.sorted_items())
for i, comp in enumerate(ical.walk()):
if comp.name == 'VEVENT':
event = {}
for name, prop in comp.property_items():
if name in ['SUMMARY', 'LOCATION']:
event[name.lower()] = prop.to_ical().decode('utf-8')
elif name == 'DTSTART':
event['start'] = {
'dateTime': prop.dt.isoformat(),
'timeZone': str(prop.dt.tzinfo)
}
elif name == 'DTEND':
event['end'] = {
'dateTime': prop.dt.isoformat(),
'timeZone': str(prop.dt.tzinfo)
}
elif name == 'SEQUENCE':
event[name.lower()] = prop
elif name == 'TRANSP':
event['transparency'] = prop.lower()
elif name == 'CLASS':
event['visibility'] = prop.lower()
elif name == 'ORGANIZER':
event['organizer'] = {
'displayName': prop.params.get('CN') or '',
'email': re.match('mailto:(.*)', prop).group(1) or ''
}
elif name == 'DESCRIPTION':
desc = prop.to_ical().decode('utf-8')
desc = desc.replace(u'\xa0', u' ')
if name.lower() in event:
event[name.lower()] = desc + '\r\n' + event[name.lower()]
else:
event[name.lower()] = desc
elif name == 'X-ALT-DESC' and 'description' not in event:
soup = BeautifulSoup(prop, 'lxml')
desc = soup.body.text.replace(u'\xa0', u' ')
if 'description' in event:
event['description'] += '\r\n' + desc
else:
event['description'] = desc
elif name == 'ATTENDEE':
if 'attendees' not in event:
event['attendees'] = []
RSVP = prop.params.get('RSVP') or ''
RSVP = 'RSVP={}'.format('TRUE:{}'.format(prop) if RSVP == 'TRUE' else RSVP)
ROLE = prop.params.get('ROLE') or ''
event['attendees'].append({
'displayName': prop.params.get('CN') or '',
'email': re.match('mailto:(.*)', prop).group(1) or '',
'comment': ROLE
# 'comment': '{};{}'.format(RSVP, ROLE)
})
# VALARM: only remind by UI popup
elif name == 'ACTION':
event['reminders'] = {'useDefault': True}
else:
# print(name)
pass
events.append(event)
return events
def cb_insert_event(request_id, response, e):
summary = response['summary'] if response and 'summary' in response else '?'
if not e:
print('({}) - Insert event {}'.format(request_id, summary))
else:
print('({}) - Exception {}'.format(request_id, e))
def main(args):
"""The usage of the Google Calendar API.
Creates a Google Calendar API service object and outputs a list of
events on the user's calendar.
"""
credentials = get_credentials(args)
http = credentials.authorize(httplib2.Http())
service = discovery.build('calendar', 'v3', http=http)
batch = service.new_batch_http_request(callback=cb_insert_event)
calendar_id = get_calendarId(service, args.cal)
events = parse_ics(args.ics)
# create event without attendees
for i, event in enumerate(events):
batch.add(service.events().insert(calendarId=calendar_id, body=event))
batch.execute(http=http)
if __name__ == '__main__':
parser = argparser()
main(parser.parse_args())
# using xclip and xdotool to simulate keyboard event
import argparse
import datetime
import os
import re
import shlex
import time
import subprocess
from pprint import pprint
from bs4 import BeautifulSoup
from icalendar import Calendar, Event, vText
def argparser():
parser = argparse.ArgumentParser(description='Outlook .ics file to gcal')
parser.add_argument('--ics', dest='ics', required=True, help='input file')
parser.add_argument(
'--cal',
dest='cal',
default='primary',
help='execute to which calendar')
parser.add_argument('--sleep', dest='sleep', type=int, default=1)
parser.add_argument('--delay', dest='delay', type=int, default=150)
return parser
def parse_ics(ics):
events = []
with open(ics, 'r') as rf:
ical = Calendar().from_ical(rf.read())
ical_config = dict(ical.sorted_items())
for i, comp in enumerate(ical.walk()):
if comp.name == 'VEVENT':
event = {}
for name, prop in comp.property_items():
if name in ['SUMMARY', 'LOCATION']:
event[name.lower()] = prop.to_ical().decode('utf-8')
elif name == 'DTSTART':
event['start'] = {
# 'dateTime': prop.dt.isoformat(),
# 'timeZone': str(prop.dt.tzinfo),
'dateTime': prop.dt
}
elif name == 'DTEND':
event['end'] = {
# 'dateTime': prop.dt.isoformat(),
# 'timeZone': str(prop.dt.tzinfo),
'dateTime': prop.dt
}
elif name == 'SEQUENCE':
event[name.lower()] = prop
elif name == 'TRANSP':
event['transparency'] = prop.lower()
elif name == 'CLASS':
event['visibility'] = prop.lower()
elif name == 'ORGANIZER':
event['organizer'] = {
'displayName': prop.params.get('CN') or '',
'email': re.match('mailto:(.*)', prop).group(1)
or ''
}
elif name == 'DESCRIPTION':
desc = prop.to_ical().decode('utf-8')
desc = desc.replace(u'\xa0', u' ')
if name.lower() in event:
event[name.lower(
)] = desc + '\r\n' + event[name.lower()]
else:
event[name.lower()] = desc
elif name == 'X-ALT-DESC' and 'description' not in event:
soup = BeautifulSoup(prop, 'lxml')
desc = soup.body.text.replace(u'\xa0', u' ')
if 'description' in event:
event['description'] += '\r\n' + desc
else:
event['description'] = desc
elif name == 'ATTENDEE':
RSVP = prop.params.get('RSVP') or ''
RSVP = 'RSVP={}'.format('TRUE:{}'.format(prop) if RSVP == 'TRUE' else RSVP)
ROLE = prop.params.get('ROLE') or ''
email = re.match('mailto:(.*)', prop).group(1) or ''
if 'attendees' not in event:
event['attendees'] = []
event['attendees'].append({
'displayName': prop.params.get('CN') or '',
'email': email,
'comment': ROLE
})
# VALARM: only remind by UI popup
elif name == 'ACTION':
event['reminders'] = {'useDefault': True}
else:
# print(name)
pass
events.append(event)
return events
def syscall(instruction):
args = shlex.split(instruction)
complete_process = subprocess.run(
args=args, check=True, stdout=subprocess.PIPE)
return complete_process
def ctrl_v(msg, sleep=1):
os.system('echo "{}" | xclip -selection clipboard'.format(msg))
time.sleep(sleep)
os.system('xdotool key ctrl+v')
def delay_type(msg, delay=150):
os.system('xdotool type --delay {} {}'.format(delay, msg))
def main(args):
events = parse_ics(args.ics)
windows_id = syscall('xdotool search --name "Google Chrome"')
windows_id = windows_id.stdout.decode('utf-8').strip('\n')
syscall('xdotool windowfocus {}'.format(windows_id))
for i, event in enumerate(events):
# gcal hotkey c - create event
os.system('xdotool key c')
# if not in the editevent page, reload and try next event
os.system('xdotool key ctrl+l')
time.sleep(1)
os.system('xdotool key ctrl+c')
url = syscall('xclip -selection clipboard -o')
url = url.stdout.decode('utf-8')
if not re.match(r'https://calendar.google.com/calendar/.*/eventedit', url):
os.system('xdotool key F5')
time.sleep(10)
break
else:
os.system('xdotool key{}'.format(' Tab'*8))
# paste summary from clipboard
ctrl_v(event['summary'], sleep=args.sleep)
# move to date
if isinstance(event['start']['dateTime'], datetime.datetime):
os.system('xdotool key Tab Tab ctrl+a')
delay_type(event['start']['dateTime'].strftime('%Y/%m/%d'), delay=args.delay)
os.system('xdotool key Tab ctrl+a')
delay_type(event['start']['dateTime'].strftime('%H:%M'), delay=args.delay)
os.system('xdotool key Tab ctrl+a')
delay_type(event['end']['dateTime'].strftime('%H:%M'), delay=args.delay)
os.system('xdotool key Tab ctrl+a')
delay_type(event['end']['dateTime'].strftime('%Y/%m/%d'), delay=args.delay)
# move to location
os.system('xdotool key{}'.format(' Tab' * 10))
elif isinstance(event['start']['dateTime'], datetime.date):
os.system('xdotool key{} space'.format(' Tab'*7))
os.system('xdotool key{}'.format(' Shift+Tab'*2))
delay_type(event['start']['dateTime'].strftime('%Y/%m/%d'), delay=args.delay)
os.system('xdotool key Tab')
delay_type(event['end']['dateTime'].strftime('%Y/%m/%d'), delay=args.delay)
# move to location
os.system('xdotool key{}'.format(' Tab' * 9))
# move to location
if 'location' in event:
ctrl_v(event['location'], sleep=args.sleep)
# type description
os.system('xdotool key{}'.format(' Tab'*8))
if 'description' in event:
ctrl_v(event['description'], sleep=args.sleep)
# move to attendees
os.system('xdotool key{}'.format(' Shift+Tab'*13))
if 'attendees' in event:
for attendee in event['attendees']:
if 'email' not in attendee:
continue
os.system('xdotool type --delay 150 {}'.format(attendee['email']))
os.system('xdotool key Return')
# save eunbank.com.twvent without sending invitation
os.system('xdotool key ctrl+s')
time.sleep(args.sleep)
os.system('xdotool key Shift+Tab Return')
print('({}/{}) - Insert {}'.format(i+1, len(events), event['summary']))
time.sleep(10)
if __name__ == '__main__':
parser = argparser()
main(parser.parse_args())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment