├── README.md ├── ical2csv.py ├── ical2txt.py └── images ├── csv.png ├── ics.png └── python.png /README.md: -------------------------------------------------------------------------------- 1 | # ical2csv w/sorted events by start date 2 | 3 | ![alt text](images/ics.png) → ![alt text](images/python.png) → ![alt text](images/csv.png) 4 | 5 | A command line Python script that will read an ics file and write it's contents to a csv file. 6 | 7 | ## Installation 8 | Download the `ical2csv.py` file. 9 | 10 | ### Dependencies: 11 | * ***icalendar*** 12 | * [**Homepage**](http://icalendar.readthedocs.org/) 13 | * [**Code**](http://github.com/collective/icalendar) 14 | * **Installation**: `pip install icalendar` 15 | * ***Python* 3** 16 | 17 | ***Note:*** pip may be called pip3 on some systems with both python2 and python3 as options. 18 | 19 | ## Usage of ical2csv 20 | 21 | Call the script and pass in the location of the ics file. 22 | 23 | Ex: `python ical2csv.py event.ics` 24 | 25 | # ical2txt w/sorted events by start date 26 | 27 | Like ical2csv.py, it parses an ics file and writes the output to a text-file. This is more of an agenda style. 28 | 29 | ## Installation of ical2txt 30 | Download the script or clone the project and get it from there. 31 | 32 | ### Dependencies for ical2txt 33 | * ***setuptools*** (just in case : pip3 install setuptools) 34 | * ***BeautifulSoup4*** (pip3 install beautifulsoup4) 35 | * ***icalendar*** (pip3 install icalendar) 36 | * [**Homepage**](http://icalendar.readthedocs.org/) 37 | * [**Code**](http://github.com/collective/icalendar) 38 | * ***recurring-ical-events*** (pip3 install recurring-ical-events) 39 | 40 | ## Usage of ical2txt 41 | 42 | Call the script and pass in the location of the ics file. 43 | 44 | Ex: `python ical2txt.py event.ics` / `python3 ical2txt.py event.ics` 45 | 46 | Note: You can limit output to a certain time period. Useful for week logs and the like: 47 | 48 | `./ical2txt.py myexport.ics 20210101 20211231` 49 | 50 | `./ical2txt.py myexport.ics 2021-01-01T00:00:00 2021-01-31T23:59:59` 51 | 52 | **NEW AS OF 2021-03-28:** Recurring events are now actually processed in ical2txt.py. If no end date is given 5 years from now is chosen. 53 | 54 | ## Contributing 55 | 56 | 1. Fork it! 57 | 2. Create your feature branch: git checkout -b my-new-feature 58 | 3. Commit your changes: git commit -am 'Add some feature' 59 | 4. Push to the branch: git push origin my-new-feature 60 | 5. Submit a pull request 😊 61 | 62 | ## Credits 63 | 64 | Lead Developer - ical2csv - [Erik Cox](https://github.com/erikcox/) 65 | 66 | Developer - ical2txt - [Martin Møller](https://github.com/martinm76) 67 | 68 | Python 3 compatibility and improvements - [bozoslivehere](https://github.com/bozoslivehere/) 69 | 70 | Logic and adjustments to sort events chronologically (Google Calendar doesn't do this in its export) - [Martin Møller](https://github.com/martinm76) 71 | 72 | Removal of HTML code from events (currently only ical2txt) - [Martin Møller](https://github.com/martinm76) 73 | 74 | ## License 75 | 76 | The MIT License (MIT) 77 | 78 | Copyright (c) 2019 Erik Cox 79 | 80 | 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: 81 | 82 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 83 | 84 | 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. 85 | -------------------------------------------------------------------------------- /ical2csv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import os.path 5 | from icalendar import Calendar 6 | import csv 7 | from datetime import datetime, date 8 | 9 | filename = sys.argv[1] 10 | # TODO: use regex to get file extension (chars after last period), in case it's not exactly 3 chars. 11 | file_extension = str(sys.argv[1])[-3:] 12 | headers = ('Summary', 'UID', 'Description', 'Location', 'Start Time', 'End Time', 'URL') 13 | 14 | class CalendarEvent: 15 | """Calendar event class""" 16 | summary = '' 17 | uid = '' 18 | description = '' 19 | location = '' 20 | start = '' 21 | end = '' 22 | url = '' 23 | 24 | def __init__(self, name): 25 | self.name = name 26 | 27 | events = [] 28 | 29 | 30 | def open_cal(): 31 | if os.path.isfile(filename): 32 | if file_extension == 'ics': 33 | print("Extracting events from file:", filename, "\n") 34 | f = open(sys.argv[1], 'rb') 35 | gcal = Calendar.from_ical(f.read()) 36 | 37 | for component in gcal.walk(): 38 | event = CalendarEvent("event") 39 | if component.get('TRANSP') == 'TRANSPARENT': continue #skip event that have not been accepted 40 | if component.get('SUMMARY') == None: continue #skip blank items 41 | event.summary = component.get('SUMMARY') 42 | event.uid = component.get('UID') 43 | if component.get('DESCRIPTION') == None: continue #skip blank items 44 | event.description = component.get('DESCRIPTION') 45 | event.location = component.get('LOCATION') 46 | if hasattr(component.get('dtstart'), 'dt'): 47 | event.start = component.get('dtstart').dt 48 | if isinstance(event.start, date): 49 | event.start = datetime.combine(event.start, datetime.min.time()) 50 | if hasattr(component.get('dtend'), 'dt'): 51 | event.end = component.get('dtend').dt 52 | if isinstance(event.end, date): 53 | event.end = datetime.combine(event.end, datetime.min.time()) 54 | 55 | 56 | event.url = component.get('URL') 57 | events.append(event) 58 | f.close() 59 | else: 60 | print("You entered ", filename, ". ") 61 | print(file_extension.upper(), " is not a valid file format. Looking for an ICS file.") 62 | exit(0) 63 | else: 64 | print("I can't find the file ", filename, ".") 65 | print("Please enter an ics file located in the same folder as this script.") 66 | exit(0) 67 | 68 | 69 | def csv_write(icsfile): 70 | csvfile = icsfile[:-3] + "csv" 71 | try: 72 | with open(csvfile, 'w') as myfile: 73 | wr = csv.writer(myfile, quoting=csv.QUOTE_ALL) 74 | wr.writerow(headers) 75 | for event in sortedevents: 76 | values = (event.summary.encode('utf8').decode(), event.uid, event.description.encode('utf8').decode(), event.location, event.start, event.end, event.url) 77 | wr.writerow(values) 78 | print("Wrote to ", csvfile, "\n") 79 | except IOError: 80 | print("Could not open file! Please close Excel!") 81 | exit(0) 82 | 83 | 84 | def debug_event(class_name): 85 | print("Contents of ", class_name.name, ":") 86 | print(class_name.summary) 87 | print(class_name.uid) 88 | print(class_name.description) 89 | print(class_name.location) 90 | print(class_name.start) 91 | print(class_name.end) 92 | print(class_name.url, "\n") 93 | 94 | open_cal() 95 | sortedevents=sorted(events, key=lambda obj: obj.start) # Needed to sort events. They are not fully chronological in a Google Calendard export ... 96 | csv_write(filename) 97 | #debug_event(event) 98 | -------------------------------------------------------------------------------- /ical2txt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import os.path 5 | from icalendar import Calendar 6 | import recurring_ical_events 7 | from bs4 import BeautifulSoup 8 | import warnings 9 | from dateutil.parser import parse 10 | import datetime 11 | 12 | warnings.filterwarnings("ignore", category=UserWarning, module='bs4') # We don't want warnings about URL's. We just what the URL printed, if there. 13 | 14 | if len(sys.argv) <= 1: 15 | print("Please call this script with an ics-file as parameter.\n") 16 | print("Even better, call it with start and end dates:\n") 17 | print(sys.argv[0] + " myexport.ics 20210101 20210201") 18 | print(sys.argv[0] + " myexport.ics 2021-01-01T00:00:00 2021-01-31T23:59:59\n") 19 | exit(1) 20 | 21 | filename = sys.argv[1] 22 | # TODO: use regex to get file extension (chars after last period), in case it's not exactly 3 chars. 23 | file_extension = str(sys.argv[1])[-3:] 24 | headers = ('Summary', 'UID', 'Description', 'Location', 'Start Time', 'End Time', 'URL') 25 | 26 | class CalendarEvent: 27 | """Calendar event class""" 28 | summary = '' 29 | uid = '' 30 | description = '' 31 | location = '' 32 | start = '' 33 | end = '' 34 | url = '' 35 | 36 | def __init__(self, name): 37 | self.name = name 38 | 39 | events = [] 40 | 41 | def removehtml(html): 42 | # Almost word for word copy from here: https://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python 43 | 44 | soup = BeautifulSoup(html, features="html.parser") 45 | # kill all script and style elements 46 | for script in soup(["script", "style"]): 47 | script.extract() # remove it 48 | 49 | text = soup.get_text() # Get plain text 50 | 51 | # break into lines and remove leading and trailing space on each 52 | lines = (line.strip() for line in text.splitlines()) 53 | # break multi-headlines into a line each 54 | chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) 55 | # drop blank lines 56 | text = '\n'.join(chunk for chunk in chunks if chunk) 57 | 58 | return text 59 | 60 | def open_cal(): 61 | if os.path.isfile(filename): 62 | if file_extension == 'ics': 63 | print("Extracting events from file:", filename, "\n") 64 | f = open(sys.argv[1], 'rb') 65 | gcal = Calendar.from_ical(f.read()) 66 | revents = recurring_ical_events.of(gcal).between(istart,istop) 67 | 68 | # for component in gcal.walk(): 69 | for component in revents: 70 | event = CalendarEvent("event") 71 | v=(dir(component).count('get')) # Only proces data if object is a valid event 72 | if (v != 0): 73 | if component.get('TRANSP') == 'TRANSPARENT': continue #skip all day events and the like 74 | if component.get('SUMMARY') == None: continue #skip blank items 75 | event.summary = component.get('SUMMARY') 76 | event.uid = component.get('UID') 77 | if component.get('DESCRIPTION') == None: continue #skip blank items 78 | event.description = component.get('DESCRIPTION') 79 | event.location = component.get('LOCATION') 80 | if hasattr(component.get('dtstart'), 'dt'): 81 | event.start = component.get('dtstart').dt 82 | if hasattr(component.get('dtend'), 'dt'): 83 | event.end = component.get('dtend').dt 84 | 85 | event.url = component.get('URL') 86 | events.append(event) 87 | f.close() 88 | else: 89 | print("You entered ", filename, ". ") 90 | print(file_extension.upper(), " is not a valid file format. Looking for an ICS file.") 91 | exit(0) 92 | else: 93 | print("I can't find the file ", filename, ".") 94 | print("Please enter an ics file located in the same folder as this script.") 95 | exit(0) 96 | 97 | 98 | def txt_write(icsfile): 99 | txtfile = icsfile[:-3] + "txt" 100 | prevdate="" 101 | spent=0 102 | evcount=0 103 | evskip=0 104 | sys.stdout.write("Processing events : ") 105 | try: 106 | with open(txtfile, 'w') as myfile: 107 | for event in sortedevents: 108 | 109 | if prevdate != event.start.strftime("%Y-%m-%d") and spent > 0: # Make a header for each day 110 | if prevdate != '': # If you don't want a summary of the time spent added, comment this section. 111 | th=divmod(spent, 3600)[0] 112 | tm=divmod(spent, 3600)[1]/60 113 | myfile.write("\nTime Total: " + '{:02.0f}'.format(th) + ":" + '{:02.0f}'.format(tm) + "\n") 114 | spent=0 115 | if event.start.timestamp() > istart.timestamp() and event.start.timestamp() < istop.timestamp(): 116 | if prevdate != event.start.strftime("%Y-%m-%d"): # Make a header for each day 117 | prevdate = event.start.strftime("%Y-%m-%d") 118 | myfile.write("\nWorklog, " + prevdate + "\n===================\n") 119 | 120 | duration = event.end - event.start 121 | ds = duration.total_seconds() 122 | spent += ds 123 | hours = divmod(ds, 3600)[0] 124 | minutes = divmod(ds,3600)[1]/60 125 | description=removehtml(event.description.encode('utf-8').decode()) 126 | values = event.start.strftime("%H:%M") + " - " + event.end.strftime("%H:%M") + " (" + '{:02.0f}'.format(hours) + ":" + '{:02.0f}'.format(minutes) + ") " + event.summary.encode('utf-8').decode() 127 | if event.location != '': values = values + " [" + event.location + "]" # Only include location if there is one 128 | 129 | # Remove Google Meet and Skype Meeting part of description 130 | trimmed=description.split('-::~')[0].split('......')[0] 131 | #print("DescLen: " + str(len(description)) + " TrimmedLen: " + str(len(trimmed)) + " : " + trimmed) # For debugging 132 | description=trimmed 133 | if description != '': 134 | values = values + "\n" + description + "\n" 135 | myfile.write(values+"\n") 136 | sys.stdout.write(".") 137 | sys.stdout.flush() 138 | evcount+=1 139 | else: 140 | sys.stdout.write("S") 141 | sys.stdout.flush() 142 | evskip+=1 143 | 144 | th=divmod(spent, 3600)[0] 145 | tm=divmod(spent, 3600)[1]/60 146 | myfile.write("\nTime Total: " + '{:02.0f}'.format(th) + ":" + '{:02.0f}'.format(tm) + "\n") 147 | 148 | print("\n\nWrote " + str(evcount) + " events to ", txtfile, " and skipped ", str(evskip), " events\n") 149 | except IOError: 150 | print("Could not open file!") 151 | exit(0) 152 | 153 | 154 | def debug_event(class_name): 155 | print("Contents of ", class_name.name, ":") 156 | print(class_name.summary) 157 | print(class_name.uid) 158 | print(class_name.description) 159 | print(class_name.location) 160 | print(class_name.start) 161 | print(class_name.end) 162 | print(class_name.url, "\n") 163 | 164 | now=datetime.datetime.now() 165 | istart=datetime.datetime.fromtimestamp(0) # Start of UNIX epoch (1970-01-01T00:00:00) 166 | istop=now+datetime.timedelta(seconds=157680000) # Stop 5 years in the future, if no enddate is given, to make sure reucurring events don't go on forever ... 167 | 168 | if len(sys.argv) > 3: 169 | if sys.argv[2] != '': 170 | istart=parse(sys.argv[2]) 171 | if sys.argv[3] != '': 172 | istop=parse(sys.argv[3]) 173 | 174 | open_cal() # Open ics file and do initial parsing of events 175 | sortedevents=sorted(events, key=lambda obj: obj.start) # Make sure events are in chronological order 176 | txt_write(filename) # Write the matching events to the textfile. With recurring_ical_events, scoping is already done. 177 | #debug_event(event) 178 | -------------------------------------------------------------------------------- /images/csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikcox/ical2csv/2842b630d8a06d63417e365e445d1320a32803a3/images/csv.png -------------------------------------------------------------------------------- /images/ics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikcox/ical2csv/2842b630d8a06d63417e365e445d1320a32803a3/images/ics.png -------------------------------------------------------------------------------- /images/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikcox/ical2csv/2842b630d8a06d63417e365e445d1320a32803a3/images/python.png --------------------------------------------------------------------------------