Small tool to synchronize Jekyll event collection with remote calendar.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

112 lines
4.1 KiB

# -*- coding: utf-8 -*-
# Copyright 2018 Jim Martens
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
"""calendarsync.calendarsync: provides entry point main()"""
import argparse
import datetime
import glob
import os
from os.path import isdir
import pkg_resources
from ics import Calendar
from urllib.request import urlopen
from urllib.parse import urlparse, ParseResult
def main() -> None:
Main entry point.
parser = argparse.ArgumentParser(
description="Synchronizes jekyll event collection with remote calendar",
parser.add_argument('calendar_url', type=validate_url, help='URL to the remote calendar ICS location')
parser.add_argument('event_collection_path', type=validate_dir, help='Path to event collection directory')
args = parser.parse_args()
sync(args.calendar_url, args.event_collection_path)
def sync(calendar_url: str, event_collection_path: str) -> None:
Synchronizes the event collection with a remote calendar.
:param calendar_url: URL to remote calendar ICS
:param event_collection_path: path to event collection directory
calendar = Calendar(urlopen(calendar_url).read().decode('utf-8'))
template_filename = pkg_resources.resource_filename(
with open(template_filename, 'r', encoding='utf-8') as template:
template_content =
# remove previous event files in directory to allow the removal of events in calendar
files = glob.glob(event_collection_path + '*.markdown')
for file in files:
for event in
event_content = template_content.replace('<name>',
created: datetime.datetime = event.created
created_str = created.isoformat(sep=' ')
event_content = event_content.replace('<date>', created_str)
begin: datetime.datetime = event.begin
begin_str = begin.isoformat(sep=' ')
event_content = event_content.replace('<begin>', begin_str)
end: datetime.datetime = event.end
end_str = end.isoformat(sep=' ')
event_content = event_content.replace('<end>', end_str)
location_info = event.location.split(sep=':')
event_content = event_content.replace('<location>', location_info[0])
if len(location_info) == 2:
event_content = event_content.replace('<address>', location_info[1])
event_content = event_content.replace('<address>', location_info[0])
event_filename = + '-' +' ', '_') + '.markdown'
with open(event_collection_path + event_filename, 'w', encoding='utf-8', newline='\n') as event_file:
def validate_url(url: str) -> str:
Validates a URL and returns it on success.
:param url: URL to verify
:return: verified URL
:raises: argparse.ArgumentTypeError if URL is invalid
parsed_url: ParseResult = urlparse(url)
if parsed_url.netloc == '' or parsed_url.scheme == '':
raise argparse.ArgumentTypeError(f"'{url}' is not a valid URL.")
return url
def validate_dir(directory: str) -> str:
Validates a directory path and returns it on success.
:param directory: path to directory to verify
:return: verified directory path
:raises: argparse.ArgumentTypeError if directory does not exist
if not isdir(directory):
raise argparse.ArgumentTypeError(f"'{directory}' is not an existing directory.")
return directory