replaced implementation

This commit is contained in:
George Ruinelli 2024-06-12 21:23:56 +02:00
parent 7385ada9b5
commit 00446bc0b6
6 changed files with 375 additions and 169 deletions

View File

@ -1,42 +1,75 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import time
import datetime
import json
from pytz import UTC # timezone
import caldav
import caldav.elements.ical # for calendarColor
import icalendar import icalendar
import recurring_ical_events
import requests import requests
import datetime
import pickle
import pprint
import json
import config as cfg import config as cfg
from collections import OrderedDict
from operator import getitem
import pprint as pp
def fetch_calendar():
numberOfDaysToShow = 5 numberOfDaysToShow = 5
#numberOfDaysToShow = 1
#excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'infos'] useCachedData = False
excludedCalendars = ['ToDo', 'ToDo Gemeinsam', 'Geburtstage von Kontakten', 'Geburtstage', 'Infos'] storeDataInCache = False
#excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'gemeinsam', 'evtermine_shared_by_anita', 'jael']
#excludedCalendars = ['gemeinsam', 'contact_birthdays', 'geburtstage']
client = caldav.DAVClient(url=cfg.url, username=cfg.userN, password=cfg.passW)
principal = client.principal()
calendars = principal.calendars()
includedCalendarIDs = []
for m in cfg.calendarsMetadata:
excluded = False
for excludedCalendar in cfg.excludedCalendarIDs:
#if excludedCalendar == m["calendarName"]:
if excludedCalendar == m["displayName"]:
excluded = True
if excluded:
continue
includedCalendarIDs.append(m["calendarName"])
def fetchCalendar(calendarName):
url = cfg.urlBase + "/" + calendarName + cfg.urlSuffix
#print(f"Fetching calendar \"{calendarName}\" from {url}...")
response = requests.get(url, auth=(cfg.userN, cfg.passW))
return response.text
def fetchCalendarsMetadata():
size = 0
icsData = {}
for id in includedCalendarIDs:
icsData[id] = fetchCalendar(id)
#print(id, icsData[id])
size += len(icsData[id])
#print(f"Fetched {(float(size) / 1024 / 1024):.4} MB")
return icsData
def get_calendar_data():
# Fetch calendars
if useCachedData:
#print("Using cached data")
with open('cacheData.pkl', 'rb') as fp:
icsData = pickle.load(fp)
else:
icsData = fetchCalendarsMetadata()
if storeDataInCache:
# save dictionary to .pkl file (for debugging only)
with open('cacheData.pkl', 'wb') as fp:
pickle.dump(icsData, fp)
#print('ICS data saved successfully to file')
# Prepare list of days
firstDay = datetime.date.today() firstDay = datetime.date.today()
lastDay = firstDay + datetime.timedelta(days=numberOfDaysToShow) lastDay = firstDay + datetime.timedelta(days=numberOfDaysToShow)
#print("Showing all events between %s and %s" % (firstDay, lastDay)) #print("Showing all events between %s and %s" % (firstDay, lastDay))
days = [] days = []
day = firstDay day = firstDay
while day < lastDay: while day < lastDay:
days.append(day) days.append(day)
@ -44,150 +77,249 @@ def fetch_calendar():
#print("Days to list: %s" % days) #print("Days to list: %s" % days)
# Get offset from local time to UTC, see also https://stackoverflow.com/questions/3168096/getting-computers-utc-offset-in-python # extracting events
# TODO: This should be done for every event individually! events = {}
ts = time.time() for day in days:
utcOffset = (datetime.datetime.fromtimestamp(ts) - dayId = day.strftime("%m/%d/%Y")
datetime.datetime.utcfromtimestamp(ts)).total_seconds() events[dayId] = {}
utcOffset = int(utcOffset / 3600) # in hours #print(f"Extracting events for {day}...")
#print("UTC offset:", utcOffset) for id in includedCalendarIDs:
calendarData = icalendar.Calendar.from_ical(icsData[id])
es = recurring_ical_events.of(calendarData).at(day)
for e in es:
#print(e)
start = e["DTSTART"].dt
startDate = start.strftime("%m/%d/%Y")
startTime = start.strftime("%H:%M")
end = e["DTEND"].dt
endDate = end.strftime("%m/%d/%Y")
endTime = end.strftime("%H:%M")
summary = str(e["SUMMARY"])
#print("All calendars:") for m in cfg.calendarsMetadata:
#for calendar in calendars: if id == m["calendarName"]:
#print(" %s" % str(calendar).split("/")[-2]) name = m["displayName"]
break
calendarsFiltered = [] d = {"calendar": name, "summary": summary,
"startDate": startDate, "startTime": startTime,
"endDate": endDate, "endTime": endTime
}
# exclude some calendars: uid = start.strftime("%H-%M") + "_" + str(e["UID"])
for calendar in calendars: events[dayId][uid] = d
#print(excludedCalendars) #print(d)
#print(calendar)
#if str(calendar).split("/")[-2] in excludedCalendars:
if str(calendar) in excludedCalendars:
#print("Ignoring %s" % str(calendar).split("/")[-2])
#print("Ignoring %s" % str(calendar))
continue
else:
#print("Adding %s" % str(calendar).split("/")[-2])
#print("Adding %s" % str(calendar))
calendarsFiltered.append(calendar)
# Sort by time
myKeys = list(events[dayId].keys())
myKeys.sort()
events[dayId] = {i: events[dayId][i] for i in myKeys}
#print("Filtered calendars:") #pprint.pprint(events)
#for calendar in calendarsFiltered:
#print(" %s" % str(calendar).split("/")[-2])
eventsList = {}
metaData = {} metaData = {}
for m in cfg.calendarsMetadata:
for day in days: #print(m)
eventsList[day] = {} if m["calendarName"] in includedCalendarIDs:
metaData[m["displayName"]] = {
#print("processing calendars") "displayname": m["displayName"],
for calendar in calendarsFiltered: "color": m["color"]
#calendarName = str(calendar).split("/")[-2] }
calendarName = str(calendar)
metaData[calendarName] = {}
metaData[calendarName]['displayname'] = list(calendar.get_properties([caldav.dav.DisplayName()]).values())[0]
metaData[calendarName]['color'] = list(calendar.get_properties([caldav.elements.ical.CalendarColor()]).values())[0]
#print(metaData)
for day in days:
#print(" Looking for events between %s and %s" % (day, day + datetime.timedelta(days=1)))
results = calendar.date_search(day, day + datetime.timedelta(days=1)) # get event for selected day (24h)
for eventraw in results:
eventUuid = str(eventraw).split("/")[-1].split(".")[0]
#print(" - %s" % eventUuid)
# print(str(eventraw))
#event = icalendar.Calendar.from_ical(eventraw._data)
url = "https://" + cfg.userN + ":" + cfg.passW + "@" + str(eventraw).replace("Event: https://", "")
# print(url)
data = requests.get(url).text
event = icalendar.Calendar.from_ical(data)
for component in event.walk():
if component.name == "VEVENT":
eventsList[day][eventUuid] = {}
##eventsList[day][eventUuid]["calendar"] = str(calendar).split("/")[-2]
eventsList[day][eventUuid]["calendar"] = str(calendar)
# print(" summary: %s" % component.get('summary'))
eventsList[day][eventUuid]["summary"] = str(component.get('summary'))
# print(" description: %s" % component.get('description'))
#eventsList[day][eventUuid]["description"] = str(component.get('description'))
startDate = component.get('dtstart')
# print(" dtstart: %s == %s?" % (startDate.dt.strftime('%m/%d/%Y %H:%M'), day))
if startDate.dt.strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event starts today
# print(" single day event")
eventsList[day][eventUuid]["startDate"] = startDate.dt.strftime('%m/%d/%Y')
eventsList[day][eventUuid]["startTime"] = (startDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M')
else: # event started before today, set startdate to day and start time to midnight (for multi day events with start/end time)
# print(" multi day event")
eventsList[day][eventUuid]["startDate"] = day.strftime('%m/%d/%Y')
# eventsList[day][eventUuid]["startTime"] = "00:00" # No longer working!
eventsList[day][eventUuid]["startTime"] = (startDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M')
try:
endDate = component.get('dtend')
#print(" dtend: %s" % endDate.dt.strftime('%m/%d/%Y %H:%M'))
if endDate.dt.strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event ends today
eventsList[day][eventUuid]["endDate"] = endDate.dt.strftime('%m/%d/%Y')
eventsList[day][eventUuid]["endTime"] = (endDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M')
else: # event ends after today, set enddate to day and end time to midnight (for multi day events with start/end time)
eventsList[day][eventUuid]["endDate"] = day.strftime('%m/%d/%Y')
# eventsList[day][eventUuid]["endTime"] = "24:00" # No longer working!
eventsList[day][eventUuid]["endDate"] = endDate.dt.strftime('%m/%d/%Y')
except AttributeError as ae: # event has no endtime, use duration
#print(" %s" % ae)
duration = component.get('duration')
#print(" Event misses end time, using duration")
#print(" duration: %s" % duration.dt)
if (startDate.dt + duration.dt).strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event ends today
eventsList[day][eventUuid]["endDate"] = (startDate.dt + duration.dt).strftime('%m/%d/%Y')
eventsList[day][eventUuid]["endTime"] = (startDate.dt + duration.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M')
else: # event ends after today, set enddate to day and end time to midnight (for multi day events with start/end time)
eventsList[day][eventUuid]["endDate"] = day.strftime('%m/%d/%Y')
eventsList[day][eventUuid]["endTime"] = "24:00"
#print('')
#break
# order by time per day, see https://www.geeksforgeeks.org/python-sort-nested-dictionary-by-key/
for day in days:
sortedbyTimePerDay = OrderedDict(sorted(eventsList[day].items(), key = lambda x: getitem(x[1], 'startTime')))
eventsList[day] = sortedbyTimePerDay
# Sort days
eventsList = OrderedDict(sorted(eventsList.items()))
# reformat day key (datetime => string)
for day in days:
eventsList[day.strftime('%m/%d/%Y')] = eventsList.pop(day)
#pp.pprint(eventsList)
data = {}
data['metadata'] = metaData
data['events'] = OrderedDict(eventsList)
#pp.pprint(data)
# Converting to JSON # Converting to JSON
dataJson = json.dumps(data, indent=4) return json.dumps({"metadata": metaData, "events": events}, indent=4)
return dataJson
#
#
#
# numberOfDaysToShow = 5
#
# #excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'infos']
# excludedCalendars = ['ToDo', 'ToDo Gemeinsam', 'Geburtstage von Kontakten', 'Geburtstage', 'Infos']
# #excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'gemeinsam', 'evtermine_shared_by_anita', 'jael']
# #excludedCalendars = ['gemeinsam', 'contact_birthdays', 'geburtstage']
#
# client = caldav.DAVClient(url=cfg.url, username=cfg.userN, password=cfg.passW)
# principal = client.principal()
# calendars = principal.calendars()
#
# firstDay = datetime.date.today()
# lastDay = firstDay + datetime.timedelta(days=numberOfDaysToShow)
# #print("Showing all events between %s and %s" % (firstDay, lastDay))
#
# days = []
#
# day = firstDay
# while day < lastDay:
# days.append(day)
# day += datetime.timedelta(days=1)
#
# #print("Days to list: %s" % days)
#
# # Get offset from local time to UTC, see also https://stackoverflow.com/questions/3168096/getting-computers-utc-offset-in-python
# # TODO: This should be done for every event individually!
# ts = time.time()
# utcOffset = (datetime.datetime.fromtimestamp(ts) -
# datetime.datetime.utcfromtimestamp(ts)).total_seconds()
# utcOffset = int(utcOffset / 3600) # in hours
# #print("UTC offset:", utcOffset)
#
# #print("All calendars:")
# #for calendar in calendars:
# #print(" %s" % str(calendar).split("/")[-2])
#
# calendarsFiltered = []
#
# # exclude some calendars:
# for calendar in calendars:
# #print(excludedCalendars)
# #print(calendar)
# #if str(calendar).split("/")[-2] in excludedCalendars:
# if str(calendar) in excludedCalendars:
# #print("Ignoring %s" % str(calendar).split("/")[-2])
# #print("Ignoring %s" % str(calendar))
# continue
# else:
# #print("Adding %s" % str(calendar).split("/")[-2])
# #print("Adding %s" % str(calendar))
# calendarsFiltered.append(calendar)
#
#
# #print("Filtered calendars:")
# #for calendar in calendarsFiltered:
# #print(" %s" % str(calendar).split("/")[-2])
#
# eventsList = {}
# metaData = {}
#
# for day in days:
# eventsList[day] = {}
#
# #print("processing calendars")
# for calendar in calendarsFiltered:
# #calendarName = str(calendar).split("/")[-2]
# calendarName = str(calendar)
# metaData[calendarName] = {}
# metaData[calendarName]['displayname'] = list(calendar.get_properties([caldav.dav.DisplayName()]).values())[0]
# metaData[calendarName]['color'] = list(calendar.get_properties([caldav.elements.ical.CalendarColor()]).values())[0]
# #print(metaData)
#
# for day in days:
# #print(" Looking for events between %s and %s" % (day, day + datetime.timedelta(days=1)))
# results = calendar.date_search(day, day + datetime.timedelta(days=1)) # get event for selected day (24h)
#
# for eventraw in results:
# eventUuid = str(eventraw).split("/")[-1].split(".")[0]
# #print(" - %s" % eventUuid)
#
# # print(str(eventraw))
# #event = icalendar.Calendar.from_ical(eventraw._data)
#
# url = "https://" + cfg.userN + ":" + cfg.passW + "@" + str(eventraw).replace("Event: https://", "")
# # print(url)
# data = requests.get(url).text
#
# event = icalendar.Calendar.from_ical(data)
# for component in event.walk():
# if component.name == "VEVENT":
# eventsList[day][eventUuid] = {}
# ##eventsList[day][eventUuid]["calendar"] = str(calendar).split("/")[-2]
# eventsList[day][eventUuid]["calendar"] = str(calendar)
#
# # print(" summary: %s" % component.get('summary'))
# eventsList[day][eventUuid]["summary"] = str(component.get('summary'))
#
# # print(" description: %s" % component.get('description'))
# #eventsList[day][eventUuid]["description"] = str(component.get('description'))
#
# startDate = component.get('dtstart')
# # print(" dtstart: %s == %s?" % (startDate.dt.strftime('%m/%d/%Y %H:%M'), day))
# if startDate.dt.strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event starts today
# # print(" single day event")
# eventsList[day][eventUuid]["startDate"] = startDate.dt.strftime('%m/%d/%Y')
# eventsList[day][eventUuid]["startTime"] = (startDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M')
# else: # event started before today, set startdate to day and start time to midnight (for multi day events with start/end time)
# # print(" multi day event")
# eventsList[day][eventUuid]["startDate"] = day.strftime('%m/%d/%Y')
# # eventsList[day][eventUuid]["startTime"] = "00:00" # No longer working!
# eventsList[day][eventUuid]["startTime"] = (startDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M')
#
# try:
# endDate = component.get('dtend')
# #print(" dtend: %s" % endDate.dt.strftime('%m/%d/%Y %H:%M'))
# if endDate.dt.strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event ends today
# eventsList[day][eventUuid]["endDate"] = endDate.dt.strftime('%m/%d/%Y')
# eventsList[day][eventUuid]["endTime"] = (endDate.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M')
# else: # event ends after today, set enddate to day and end time to midnight (for multi day events with start/end time)
# eventsList[day][eventUuid]["endDate"] = day.strftime('%m/%d/%Y')
# # eventsList[day][eventUuid]["endTime"] = "24:00" # No longer working!
# eventsList[day][eventUuid]["endDate"] = endDate.dt.strftime('%m/%d/%Y')
#
# except AttributeError as ae: # event has no endtime, use duration
# #print(" %s" % ae)
# duration = component.get('duration')
# #print(" Event misses end time, using duration")
# #print(" duration: %s" % duration.dt)
# if (startDate.dt + duration.dt).strftime('%m/%d/%Y') == day.strftime('%m/%d/%Y'): # event ends today
# eventsList[day][eventUuid]["endDate"] = (startDate.dt + duration.dt).strftime('%m/%d/%Y')
# eventsList[day][eventUuid]["endTime"] = (startDate.dt + duration.dt + datetime.timedelta(hours=utcOffset)).strftime('%H:%M')
# else: # event ends after today, set enddate to day and end time to midnight (for multi day events with start/end time)
# eventsList[day][eventUuid]["endDate"] = day.strftime('%m/%d/%Y')
# eventsList[day][eventUuid]["endTime"] = "24:00"
#
# #print('')
# #break
#
#
# # order by time per day, see https://www.geeksforgeeks.org/python-sort-nested-dictionary-by-key/
# for day in days:
# sortedbyTimePerDay = OrderedDict(sorted(eventsList[day].items(), key = lambda x: getitem(x[1], 'startTime')))
# eventsList[day] = sortedbyTimePerDay
#
# # Sort days
# eventsList = OrderedDict(sorted(eventsList.items()))
#
#
# # reformat day key (datetime => string)
# for day in days:
# eventsList[day.strftime('%m/%d/%Y')] = eventsList.pop(day)
#
#
# #pp.pprint(eventsList)
#
# data = {}
# data['metadata'] = metaData
# data['events'] = OrderedDict(eventsList)
#
# #pp.pprint(data)
#
#
# # Converting to JSON
# dataJson = json.dumps(data, indent=4)
# return dataJson
if __name__ == '__main__': if __name__ == '__main__':
print(fetch_calendar()) print(get_calendar_data())

View File

@ -1,6 +1,26 @@
#!/usr/bin/env python #!/usr/bin/env python
# Configuration for the CalDAV server # Configuration for the CalDAV server
url = "https://cloud.ruinelli.ch/remote.php/dav/principals/users/" urlBase = 'https://cloud.ruinelli.ch/remote.php/dav/calendars/gruinelli'
urlSuffix = "?export"
userN = "gruinelli" userN = "gruinelli"
passW = "attgigoc1" passW = "attgigoc1"
"""calendar metaData
Fetched with the helper.py script"""
calendarsMetadata = [
{'calendarName': 'george', 'color': '#55aaff', 'displayName': 'George'},
{'calendarName': 'gemeinsam', 'color': '#ffff00', 'displayName': 'Gemeinsam'},
{'calendarName': 'todo', 'color': '#ffaaff', 'displayName': 'ToDo'},
{'calendarName': 'todogemeinsam', 'color': '#cc66cc', 'displayName': 'ToDo Gemeinsam'},
{'calendarName': 'contact_birthdays', 'color': '#FFFFCA', 'displayName': 'Geburtstage von Kontakten'},
{'calendarName': 'geburtstage', 'color': '#74e7d2', 'displayName': 'Geburtstage'},
{'calendarName': 'jael', 'color': '#16fb04', 'displayName': 'Kinder'},
{'calendarName': 'infos', 'color': '#0090a1', 'displayName': 'Infos'},
{'calendarName': 'anita_shared_by_anita', 'color': '#FF8000', 'displayName': 'Anita'},
{'calendarName': 'anitagesch%C3%A4ft%28bga%2Cpa%29_shared_by_anita', 'color': '#007B00', 'displayName': 'Anita Arbeit (Alixon, PAI)'},
{'calendarName': 'evtermine_shared_by_anita', 'color': '#FFE5CC', 'displayName': 'ev Termine'}
]
excludedCalendarIDs = ['ToDo', 'ToDo Gemeinsam', 'Geburtstage von Kontakten', 'Geburtstage', 'Infos']

51
helper.py Normal file
View File

@ -0,0 +1,51 @@
"""Script to fetch the calendar names and colors"""
import caldav
import caldav.elements.ical # for calendarColor
import pprint
# Configuration for the CalDAV server
url = "https://cloud.ruinelli.ch/remote.php/dav/principals/users/"
userN = "gruinelli"
passW = "attgigoc1"
def fetch_calendar():
numberOfDaysToShow = 5
# excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'infos']
excludedCalendars = ['ToDo', 'ToDo Gemeinsam', 'Geburtstage von Kontakten', 'Geburtstage', 'Infos']
# excludedCalendars = ['todo', 'todogemeinsam', 'contact_birthdays', 'geburtstage', 'gemeinsam', 'evtermine_shared_by_anita', 'jael']
# excludedCalendars = ['gemeinsam', 'contact_birthdays', 'geburtstage']
client = caldav.DAVClient(url=url, username=userN, password=passW)
principal = client.principal()
calendars = principal.calendars()
#print(calendars)
#print("")
calendarNames = (str(calendars).replace("Calendar(https://cloud.ruinelli.ch/remote.php/dav/calendars/gruinelli/", "").
replace(")", "").replace("/", "").replace("[", "").replace("]", "").
split(","))
#print(calendarNames)
#print("")
metaData = []
i = 0
for calendar in calendars:
#print(calendar)
data = {}
data["calendarName"] = calendarNames[i]
i += 1
data["displayname"] = list(calendar.get_properties([caldav.dav.DisplayName()]).values())[0]
data["color"] = list(calendar.get_properties([caldav.elements.ical.CalendarColor()]).values())[0]
metaData.append(data)
print("")
pprint.pprint(metaData)
if __name__ == '__main__':
fetch_calendar()

View File

@ -1,4 +1,6 @@
# Docker # Docker
## Local run for testing
`python webserver.py`
## Build it manually ## Build it manually
`docker build -t calendar-fetcher .` `docker build -t calendar-fetcher .`
@ -16,4 +18,3 @@ docker stop calendar-fetcher
docker rm calendar-fetcher docker rm calendar-fetcher
docker run -d -P --name calendar-fetcher -p 8014:8014 -v /volume1/web/smartmirror/data:/data --label=com.centurylinklabs.watchtower.enable=false calendar-fetcher docker run -d -P --name calendar-fetcher -p 8014:8014 -v /volume1/web/smartmirror/data:/data --label=com.centurylinklabs.watchtower.enable=false calendar-fetcher
``` ```

View File

@ -1,2 +1,6 @@
pytz icalendar
caldav recurring_ical_events
requests
datetime
#pickle
#pprint

View File

@ -3,7 +3,7 @@
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
from calendar_fetcher import fetch_calendar from calendar_fetcher import *
internalPort = 8014 internalPort = 8014
@ -29,27 +29,25 @@ class myHandler(BaseHTTPRequestHandler):
self.wfile.write(b"<h1>Calendar Fetcher</h1>\n") self.wfile.write(b"<h1>Calendar Fetcher</h1>\n")
self.wfile.write(b"fetching...<br>\n") self.wfile.write(b"fetching...<br>\n")
print("fetching...") print("fetching and processing...")
self.wfile.flush() self.wfile.flush()
data = fetch_calendar() data = get_calendar_data()
self.wfile.write(b"storing...<br>\n") self.wfile.write(b"storing...<br>\n")
print("storing...")
self.wfile.flush() self.wfile.flush()
print("storing...")
f = open(outfile, "w") f = open(outfile, "w")
f.write(data) f.write(data)
f.close() f.close()
try: # Might trow a "[Errno 32] Broken pipe" becasue the client closed the socket already (eg. due to a timeout)
self.wfile.write(b"Done.\nData got written to %s}<br>\n" % bytes(outfile, "utf8")) self.wfile.write(b"Done.\nData got written to %s}<br>\n" % bytes(outfile, "utf8"))
print("Done.\nData got written to %s}" % outfile)
self.wfile.flush() self.wfile.flush()
except:
pass
print("Done.\nData got written to %s}" % outfile)
if __name__ == '__main__': if __name__ == '__main__':