|
@@ -0,0 +1,465 @@
|
|
|
+
|
|
|
+#!/usr/bin/env python3
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""
|
|
|
+efbeff 2024-04-16
|
|
|
+
|
|
|
+Dieses script läuft z.Zt. produktiv auf Duesseldorf blech02
|
|
|
+
|
|
|
+Freifunk Dortmund Terminerinnerungen aus dem Kalender erzeugen
|
|
|
+und über die Mailingliste(n) verschicken.
|
|
|
+
|
|
|
+Die Erinnerung für das Monatstreffen wird 5 Tage vorher verschickt,
|
|
|
+die restlichen (FF@HOME, ... 2 Tage vorher )
|
|
|
+Unterschieden wird es durch den ical tag SUMMARY.
|
|
|
+Aus der WEBseite lassen sich die Termine leider nur monatsweise lesen,
|
|
|
+deshalb werden 2 Monatskalender gelesen, falls das Prüfintervall über eine
|
|
|
+Monatsgrenze geht.
|
|
|
+
|
|
|
+Das alte script von FFdo als Basis erheblich überarbeitet
|
|
|
+Dies ist die zweite Überarbeitung mit erstellen von *.ics Anhängen
|
|
|
+Dies ist die dritte Überarbeitung mit Entfernen HTML Kommentar
|
|
|
+
|
|
|
+Es darf nur 1mal am Tag laufen, sonst gibt es mehrfache Erinnerungen
|
|
|
+
|
|
|
+Der Absender reminder@freifunk-dortmund.de ist keine echte mailadresse,
|
|
|
+sondern nur als Nichtmitglied aber sendeberechtigt zugelassen in der
|
|
|
+ DEFAULT_LISTE, VEREIN_LISTE und INFRA_LISTE
|
|
|
+Dafür in der Listenadministration jeweils unter
|
|
|
+ Abo-regeln und Adressfilter
|
|
|
+ Absender-filter
|
|
|
+ Anti-Spam-filter
|
|
|
+ reminder@freifunk-dortmund.de
|
|
|
+eintragen.
|
|
|
+
|
|
|
+Mails an reminder@freifunk-dortmund.de werden abgewiesen.
|
|
|
+
|
|
|
+"""
|
|
|
+# testmode 0 setzen für Produktion
|
|
|
+# testmode 1 nur aufbereiten und in log schreiben
|
|
|
+# testmode 2 über smarthost an einzelnen Empfänger schicken
|
|
|
+# setzt eventuell username PW und port voraus
|
|
|
+# ACHTUNG PW im Klartext
|
|
|
+testmode = 0
|
|
|
+
|
|
|
+# bei testmode 1 wird keine mail versandt, die Erinnerungen werden
|
|
|
+# nur aufbereitet und ins log geschrieben
|
|
|
+
|
|
|
+import logging
|
|
|
+import logging.handlers
|
|
|
+import sys
|
|
|
+import traceback
|
|
|
+import os
|
|
|
+import shutil
|
|
|
+
|
|
|
+import smtplib
|
|
|
+import requests
|
|
|
+from email.message import EmailMessage
|
|
|
+from datetime import datetime, timedelta, date
|
|
|
+########from icalendar import Calendar, Event
|
|
|
+import re
|
|
|
+
|
|
|
+ABSFILE=os.path.abspath(__file__)
|
|
|
+CALURL_TEMPLATE='http://www.freifunk-dortmund.de/termine/{}-{:02d}/?ical=1'
|
|
|
+
|
|
|
+# empfängerlisten
|
|
|
+DEFAULT_LISTE="freifunk-do@list.free.de"
|
|
|
+INFRA_LISTE="freifunk-do-infra@list.free.de"
|
|
|
+VEREIN_LISTE="freifunk-do-verein@list.free.de"
|
|
|
+
|
|
|
+# Absender
|
|
|
+MAIL_FROM="FF-DO Termine <reminder@freifunk-dortmund.de>" if testmode < 2 else\
|
|
|
+"fbeythien@gmx.de"
|
|
|
+
|
|
|
+MAIL_SUBJECT="Monatstreffen" # wird angepasst
|
|
|
+if testmode < 2:
|
|
|
+ MAIL_HOST="list.free.de"
|
|
|
+ MAIL_STARTTLS=False
|
|
|
+ MAIL_USER=""
|
|
|
+ MAIL_PASSWORD=""
|
|
|
+ MAIL_PORT="25"
|
|
|
+else:
|
|
|
+ MAIL_HOST="mail.gmx.net"
|
|
|
+ MAIL_STARTTLS=True
|
|
|
+ MAIL_USER="xxxx"
|
|
|
+ MAIL_PASSWORD="xxxx"
|
|
|
+ MAIL_PORT="587"
|
|
|
+ DEFAULT_LISTE="efbe@prima.de"
|
|
|
+ INFRA_LISTE=DEFAULT_LISTE
|
|
|
+ VEREIN_LISTE=DEFAULT_LISTE
|
|
|
+
|
|
|
+
|
|
|
+# Tagesordnungspunkte Monatstreffen
|
|
|
+TOP_LINK="https://wiki.ffdo.de/Community/TOPs"
|
|
|
+TOP_LINK_PAGE= TOP_LINK +".page"
|
|
|
+
|
|
|
+WOTAGE = ("Montag","Dienstag","Mittwoch","Donnerstag","Freitag",
|
|
|
+ "Samstag","Sonntag")
|
|
|
+
|
|
|
+REM_TREFFEN_MONAT = 4 # Tage vorher erinnern Monatstreffen
|
|
|
+REM_TREFFEN_TOPS = REM_TREFFEN_MONAT + 2 # Tage vorher erinnern TOP ergänzen
|
|
|
+REM_STANDARD = 2 # Tage vorher erinnern FF@home, ...
|
|
|
+REM_INTERVALL = 10 # heute + x Tage Termine prüfen
|
|
|
+
|
|
|
+# logging init
|
|
|
+logging.basicConfig(level=logging.DEBUG)
|
|
|
+mylog = logging.getLogger("remind")
|
|
|
+
|
|
|
+sysl = logging.handlers.SysLogHandler(address='/dev/log')
|
|
|
+#formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
+formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
|
|
|
+sysl.setFormatter(formatter)
|
|
|
+mylog.addHandler(sysl)
|
|
|
+
|
|
|
+def print_exc_plus():
|
|
|
+ """
|
|
|
+ Print the usual traceback information, followed by a listing of all the
|
|
|
+ local variables in each frame.
|
|
|
+ """
|
|
|
+ tb = sys.exc_info()[2]
|
|
|
+ while 1:
|
|
|
+ if not tb.tb_next:
|
|
|
+ break
|
|
|
+ tb = tb.tb_next
|
|
|
+ stack = []
|
|
|
+ f = tb.tb_frame
|
|
|
+ while f:
|
|
|
+ stack.append(f)
|
|
|
+ f = f.f_back
|
|
|
+ stack.reverse()
|
|
|
+ traceback.print_exc()
|
|
|
+ print("Locals by frame, innermost last")
|
|
|
+ for frame in stack:
|
|
|
+ print()
|
|
|
+ print("Frame %s in %s at line %s" % (frame.f_code.co_name,
|
|
|
+ frame.f_code.co_filename,
|
|
|
+ frame.f_lineno))
|
|
|
+ for key, value in frame.f_locals.items():
|
|
|
+ print("\t%20s =" % key,end=" ")
|
|
|
+ #We have to be careful not to cause a new error in our error
|
|
|
+ #printer! Calling str() on an unknown object could cause an
|
|
|
+ #error we don't want.
|
|
|
+ try:
|
|
|
+ print(value)
|
|
|
+ except:
|
|
|
+ print( "<ERROR WHILE PRINTING VALUE>")
|
|
|
+
|
|
|
+
|
|
|
+#falls remind nicht erfolgreich beim cron.daily Lauf (z.B. temporäre Netzproblem)
|
|
|
+#dann versuche es in cron.hourly bis es klappt
|
|
|
+def falls_daily_versuche_hourly(progfile):
|
|
|
+ (pfad, datei) = os.path.split(progfile)
|
|
|
+ if pfad[-6:] == ".daily":
|
|
|
+ hpfad = pfad[:-5] + "hourly/"
|
|
|
+ shutil.copy2(progfile, hpfad, follow_symlinks=False)
|
|
|
+ mylog.info("remind nach cron.hourly kopiert.")
|
|
|
+
|
|
|
+# evtl remind aus hourly wieder entfernen nach erfolgreichem Lauf
|
|
|
+def falls_hourly_loe(progfile):
|
|
|
+ if "hourly" in progfile:
|
|
|
+ os.remove(progfile)
|
|
|
+ mylog.info("remind in cron.hourly gelöscht")
|
|
|
+
|
|
|
+# alle gefundenen Erinnerungen versenden wenn kein testmode gesetzt
|
|
|
+def send(msgs, sender, server, username=None, password=None):
|
|
|
+ if testmode == 0:
|
|
|
+ s = smtplib.SMTP(server, port=MAIL_PORT)
|
|
|
+# s.set_debuglevel(2)
|
|
|
+
|
|
|
+ for m in msgs:
|
|
|
+ s.send_message(m)
|
|
|
+ mylog.info(str(m))
|
|
|
+# print('\n', str(m), '\n')
|
|
|
+ s.quit()
|
|
|
+ elif testmode == 1:
|
|
|
+ for m in msgs:
|
|
|
+ mylog.info(str(m))
|
|
|
+ # print('\n', str(m), '\n')
|
|
|
+ elif testmode == 2:
|
|
|
+ with smtplib.SMTP(MAIL_HOST, port=MAIL_PORT) as s:
|
|
|
+ s.set_debuglevel(2)
|
|
|
+ s.starttls()
|
|
|
+ s.login(MAIL_USER, MAIL_PASSWORD, initial_response_ok=True)
|
|
|
+ for m in msgs:
|
|
|
+ s.send_message(m)
|
|
|
+ mylog.info(str(m))
|
|
|
+# print('\n', str(m), '\n')
|
|
|
+ s.close()
|
|
|
+ else:
|
|
|
+ mylog.info(" testmode nicht 0 - 2 ungültig.")
|
|
|
+
|
|
|
+
|
|
|
+# Erinnerung für allgemeinen Termin aufbereiten
|
|
|
+def gen_mail_allg(sender, rcpt, subject, description, vorspann, event, dtstart):
|
|
|
+ body = "DESCRIPTION " + description + " " + "\n\nDein freundlicher FFDO Autoreminder\n.\n"
|
|
|
+ msg = EmailMessage()
|
|
|
+ msg["From"]=sender
|
|
|
+ msg["To"]=rcpt
|
|
|
+ msg["Subject"]="Reminder " + subject
|
|
|
+ msg.set_content(body)
|
|
|
+ att = vorspann + event + '\r\nEND:VCALENDAR'
|
|
|
+ evdat = dtstart.date()
|
|
|
+ msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
|
|
|
+ return msg
|
|
|
+
|
|
|
+# die mmarkdown Markierungen teilweise umsetzen
|
|
|
+# --- bis ... markdown vorspann löschen
|
|
|
+# führende Leerstellen erhalten
|
|
|
+# dies ist rudimentär, kein markdown interpreter!!
|
|
|
+
|
|
|
+def zeilen_aufbereiten(tops):
|
|
|
+
|
|
|
+# html kommentar entfernen
|
|
|
+
|
|
|
+# beispiel für spamschutz entfernen
|
|
|
+# email = "tony@tiremove_thisger.net"
|
|
|
+# m = re.search("remove_this", email)
|
|
|
+# email[:m.start()] + email[m.end():]
|
|
|
+# Ergebnis: 'tony@tiger.net'
|
|
|
+
|
|
|
+ koma = re.search("<!--", tops)
|
|
|
+ kome = re.search("-->", tops)
|
|
|
+ topn = tops
|
|
|
+ if koma != None and kome != None:
|
|
|
+ if koma.end() < kome.start():
|
|
|
+ topn = tops[:koma.start()] + tops[kome.end():]
|
|
|
+
|
|
|
+ topsp = topn.partition("...")
|
|
|
+ if topsp[2]:
|
|
|
+ lines = topsp[2].split("\n")
|
|
|
+ else:
|
|
|
+ lines = topsp[0].split("\n")
|
|
|
+
|
|
|
+ body = ""
|
|
|
+ for l in lines:
|
|
|
+# print(l)
|
|
|
+ if len(l.strip()) > 0:
|
|
|
+ if l.strip()[0] == "*":
|
|
|
+ prefix = l.partition("*")[0]
|
|
|
+ body += prefix + " - " + l.strip(' *\n') + "\n"
|
|
|
+ elif l.strip()[0] == "#":
|
|
|
+ continue
|
|
|
+ elif l.strip()[0] == "-":
|
|
|
+ prefix = l.partition("-")[0]
|
|
|
+ body += prefix + "- " + l.strip(' -\n') + "\n"
|
|
|
+ else:
|
|
|
+ body += l.rstrip(" \n") + "\n"
|
|
|
+ return body
|
|
|
+
|
|
|
+# Erinnerung für TOPs einstellen zum Monatstreffen aufbereiten
|
|
|
+def gen_mail_top(sender, rcpt, subject, tops, vorspann, event, dtstart):
|
|
|
+
|
|
|
+ body = "Hallo zusammen!\n\nIn zwei Tagen wird die Erinnerung für das nächste Monatstreffen verschickt. Bisher sind folgende TOPs eingetragen:\n\n"
|
|
|
+ body += zeilen_aufbereiten(tops)
|
|
|
+ body += "\nFalls du noch ein weiteres Thema besprechen möchtest, trag es bitte hier ein: " + TOP_LINK + "\n\nDein freundlicher FFDO Auto-Reminder\n.\n"
|
|
|
+ msg = EmailMessage()
|
|
|
+ msg["From"]=sender
|
|
|
+ msg["To"]=rcpt
|
|
|
+ msg.set_content(body)
|
|
|
+ att = vorspann + event + '\r\nEND:VCALENDAR'
|
|
|
+ evdat = dtstart.date()
|
|
|
+ msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
|
|
|
+ msg["Subject"]="Reminder TOPs für " + subject
|
|
|
+ return msg
|
|
|
+
|
|
|
+# Erinnerung für Monatstreffen aufbereiten
|
|
|
+def gen_mail_monat(sender, rcpt, subject, tops, vorspann, event, dtstart):
|
|
|
+
|
|
|
+ wotag = WOTAGE[dtstart.weekday()]
|
|
|
+
|
|
|
+ stzeit = str(dtstart.time())[:5]
|
|
|
+ body = "Hallo zusammen! \n\nAm kommenden " + wotag + " um " + stzeit + " Uhr steht das nächste Monatstreffen auf dem Plan. Bisher sind folgende TOPs eingetragen: \n\n"
|
|
|
+ body += zeilen_aufbereiten(tops)
|
|
|
+ body += "\nFalls du noch ein weiteres Thema besprechen möchtest, trag es bitte hier ein: " + TOP_LINK + " \n\nDein freundlicher Auto-Reminder\n.\n"
|
|
|
+
|
|
|
+ msg = EmailMessage()
|
|
|
+ msg["From"]=sender
|
|
|
+ msg["To"]=rcpt
|
|
|
+ msg.set_content(body)
|
|
|
+ att = vorspann + event + '\r\nEND:VCALENDAR'
|
|
|
+ evdat=dtstart.date()
|
|
|
+ msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
|
|
|
+ msg["Subject"]="Reminder " + subject
|
|
|
+ return msg
|
|
|
+
|
|
|
+# key in string suchen ende mit crlf
|
|
|
+def s_in_event(string,such):
|
|
|
+ ergebnis = ''
|
|
|
+ inda=string.find(such)
|
|
|
+ if inda > -1:
|
|
|
+ inda += len(such)
|
|
|
+ inde=string[inda:].find('\r\n')
|
|
|
+ ergebnis=string[inda:inda + inde]
|
|
|
+ return ergebnis
|
|
|
+
|
|
|
+def main():
|
|
|
+ start = date.today()
|
|
|
+
|
|
|
+ # Intervall für prüfungen festlegen
|
|
|
+ end = start + timedelta(days=REM_INTERVALL)
|
|
|
+ if testmode: # bei test auch 2 vorige Tage prüfen
|
|
|
+ start -= timedelta(2)
|
|
|
+
|
|
|
+ mylog.info("Start remind testmode: " + str(testmode))
|
|
|
+ remstandard = start + timedelta(days=REM_STANDARD)
|
|
|
+ remmontreffen = start + timedelta(days=REM_TREFFEN_MONAT)
|
|
|
+ remmontops = start + timedelta(days=REM_TREFFEN_TOPS)
|
|
|
+
|
|
|
+
|
|
|
+# jetzt die Kalenderdaten einsammeln und zerlegen
|
|
|
+ kals = []
|
|
|
+ msgs = []
|
|
|
+ msguids = []
|
|
|
+ tops = None
|
|
|
+
|
|
|
+ icss = []
|
|
|
+ calurls = []
|
|
|
+# termin URL für lfd Monat erzeugen
|
|
|
+ calurls.append(CALURL_TEMPLATE.format(start.year,start.month))
|
|
|
+ if start.month != end.month:
|
|
|
+# termin URL für nächsten Monat erzeugen
|
|
|
+ calurls.append(CALURL_TEMPLATE.format(end.year,end.month))
|
|
|
+
|
|
|
+# Termindaten holen (1 oder 2 Monate) und in Kalenderformat bringen
|
|
|
+# und auch als ics daten behalten
|
|
|
+ for calurl in calurls:
|
|
|
+ try:
|
|
|
+ ics = requests.get(calurl)
|
|
|
+# gibt es Termine für den lfd / nächsten Monat
|
|
|
+ if ics:
|
|
|
+ icss.append(ics.text)
|
|
|
+ except ConnectionError as ce:
|
|
|
+ mylog.error("Fehler bei Holen Kalender: " + str(ce))
|
|
|
+ continue
|
|
|
+
|
|
|
+# Events isolieren und auswerten
|
|
|
+ veanf=0
|
|
|
+ voreins=True
|
|
|
+ icswk=ics.text
|
|
|
+ while veanf > -1:
|
|
|
+ veanf=icswk.find('BEGIN:VEVENT')
|
|
|
+ if veanf == -1: break # keine events mehr Ende while
|
|
|
+ if voreins: # Kalendervorspann retten
|
|
|
+ voreins=False
|
|
|
+ vorspann=icswk[:veanf]
|
|
|
+ if testmode > 1:
|
|
|
+ print(len(vorspann))
|
|
|
+ print(vorspann)
|
|
|
+
|
|
|
+ veend=icswk.find('END:VEVENT')
|
|
|
+ vevent=icswk[veanf:veend+len('END:VEVENT')]
|
|
|
+
|
|
|
+ #icswk setzen damit ein continue später klappt
|
|
|
+ icswk=icswk[veend+len('END:VEVENT'):]
|
|
|
+
|
|
|
+ vstdat=vevent.find('DTSTART')
|
|
|
+ vstdat1=vevent[vstdat:].find(':')
|
|
|
+ vdatum=vevent[vstdat+vstdat1+1:vstdat+vstdat1+16]
|
|
|
+ dtstart=datetime.strptime(vdatum, '%Y%m%dT%H%M%S')
|
|
|
+ if testmode > 1:
|
|
|
+ print(len(vevent))
|
|
|
+ print(vevent)
|
|
|
+ print(vdatum)
|
|
|
+ print(dtstart)
|
|
|
+ if dtstart.date() >= start and ( dtstart.date() <= end or testmode):
|
|
|
+ if testmode > 1:
|
|
|
+ print('\n\n selektiert, im Intervall\n\n')
|
|
|
+
|
|
|
+ subject = s_in_event(vevent,'SUMMARY:')
|
|
|
+ subject += ' ' +str(dtstart)
|
|
|
+
|
|
|
+ description = s_in_event(vevent,'DESCRIPTION:')
|
|
|
+ for such in ('\\n', '\\,'):
|
|
|
+ ix = description.find(such)
|
|
|
+ while ix > 0:
|
|
|
+ description = description[:ix] + description[ix+2]
|
|
|
+ ix = description.find(such)
|
|
|
+
|
|
|
+ location = s_in_event(vevent,'LOCATTION:')
|
|
|
+
|
|
|
+ url = s_in_event(vevent,'URL:')
|
|
|
+
|
|
|
+ description += '\n' + location + '\n' + url
|
|
|
+
|
|
|
+ uid = s_in_event(vevent,'UID:')
|
|
|
+ if uid in msguids:
|
|
|
+ if testmode:
|
|
|
+ print("doppelt ", uid)
|
|
|
+ continue
|
|
|
+
|
|
|
+# an FF-DO liste per default
|
|
|
+ liste = DEFAULT_LISTE
|
|
|
+
|
|
|
+# subject/SUMMARY für Ermittlung der Vorwarnzeit prüfen
|
|
|
+ if "Freifunktreffen" in subject or\
|
|
|
+ "Monatstreffen" in subject or\
|
|
|
+ "Orga-Treff" in subject:
|
|
|
+ if testmode or dtstart.date() == remmontops:
|
|
|
+# Aufforderung TOPs einzustellen
|
|
|
+ try:
|
|
|
+ tops = requests.get(TOP_LINK_PAGE)
|
|
|
+ except ConnectionError as ce:
|
|
|
+ mylog.error("Fehler holen TOPs mit requests.get(): " + str(ce))
|
|
|
+ continue
|
|
|
+
|
|
|
+ m = gen_mail_top(MAIL_FROM, liste, subject, tops.text,
|
|
|
+ vorspann, vevent, dtstart)
|
|
|
+ msgs.append(m)
|
|
|
+ msguids.append(uid)
|
|
|
+ elif testmode or dtstart.date() == remmontreffen:
|
|
|
+# Tagesordnungspunkte für nächstes Monatstreffen
|
|
|
+ try:
|
|
|
+ tops = requests.get(TOP_LINK_PAGE)
|
|
|
+ except ConnectionError as ce:
|
|
|
+ mylog.error("Fehler holen TOPs mit requests.get(): " + str(ce))
|
|
|
+ continue
|
|
|
+
|
|
|
+ m = gen_mail_monat(MAIL_FROM, liste, subject, tops.text,
|
|
|
+ vorspann, vevent, dtstart)
|
|
|
+ msgs.append(m)
|
|
|
+ msguids.append(uid)
|
|
|
+
|
|
|
+ elif "FF@home" in subject:
|
|
|
+ if testmode or dtstart.date() == remstandard:
|
|
|
+ liste = INFRA_LISTE
|
|
|
+ m = gen_mail_allg(MAIL_FROM, liste, subject,
|
|
|
+ description, vorspann, vevent, dtstart)
|
|
|
+ msgs.append(m)
|
|
|
+ msguids.append(uid)
|
|
|
+
|
|
|
+ elif "Öffentlichkeitsarbeit" in subject:
|
|
|
+ if testmode or dtstart.date() == remstandard:
|
|
|
+ liste = VEREIN_LISTE
|
|
|
+ m = gen_mail_allg(MAIL_FROM, liste, subject,
|
|
|
+ description, vorspann, vevent, dtstart)
|
|
|
+ msgs.append(m)
|
|
|
+ msguids.append(uid)
|
|
|
+ else:
|
|
|
+ if testmode or dtstart.date() == remstandard:
|
|
|
+ m = gen_mail_allg(MAIL_FROM, liste, subject,
|
|
|
+ description, vorspann, vevent, dtstart)
|
|
|
+ msgs.append(m)
|
|
|
+ msguids.append(uid)
|
|
|
+
|
|
|
+### willi=willi ### für exception test
|
|
|
+# Erinnerungen verschicken wenn kein testmode oder testmode 2
|
|
|
+ if len(msgs):
|
|
|
+ send(msgs, MAIL_FROM, MAIL_HOST, MAIL_USER, MAIL_PASSWORD)
|
|
|
+
|
|
|
+# print("\n-------", len(msgs), "Erinnerung(en)\n")
|
|
|
+ mylog.info("Ende " + str(len(msgs)) + " Erinnerungen")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ try:
|
|
|
+ main()
|
|
|
+ except:
|
|
|
+ falls_daily_versuche_hourly(ABSFILE)
|
|
|
+ traceback.print_exc()
|
|
|
+#### print_exc_plus() #für bessere Fehlersuche aktivieren
|
|
|
+ mylog.error(traceback.format_exc())
|
|
|
+ else:
|
|
|
+ falls_hourly_loe(ABSFILE)
|
|
|
+ finally:
|
|
|
+ logging.shutdown()
|
|
|
+ exit(0)
|