Skip to content
This repository has been archived by the owner on Jul 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #138 from haslersn/validate
Browse files Browse the repository at this point in the history
chore(kontaktdaten): Better error msgs when kontaktdaten.json invalid
  • Loading branch information
JuliusJacobitz authored May 27, 2021
2 parents 08ad856 + 62bc94e commit 2017cac
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 73 deletions.
134 changes: 63 additions & 71 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
except:
pass

from tools.utils import create_missing_dirs, remove_prefix
from tools.its import ImpfterminService
from tools.kontaktdaten import get_kontaktdaten, validate_kontaktdaten, ValidationError
from tools.utils import create_missing_dirs, remove_prefix

PATH = os.path.dirname(os.path.realpath(__file__))

Expand All @@ -34,8 +35,8 @@ def update_kontaktdaten_interactive(

assert (command in ["code", "search"])

if filepath is None:
filepath = os.path.join(PATH, "data/kontaktdaten.json")
# Werfe Fehler, falls die übergebenen Kontaktdaten bereits ungültig sind.
validate_kontaktdaten(known_kontaktdaten)

kontaktdaten = copy.deepcopy(known_kontaktdaten)

Expand All @@ -47,86 +48,80 @@ def update_kontaktdaten_interactive(
"https://github.com/iamnotturner/vaccipy/wiki/Ein-Code-fuer-mehrere-Impfzentren\n\n"
"Trage nun die PLZ deines Impfzentrums ein. Für mehrere Impfzentren die PLZ's kommagetrennt nacheinander.\n"
"Beispiel: 68163, 69124, 69469\n")
plz_impfzentren = input("> PLZ's der Impfzentren: ")
kontaktdaten["plz_impfzentren"] = list(
set([plz.strip() for plz in plz_impfzentren.split(",")]))
input_kontaktdaten_key(kontaktdaten,
["plz_impfzentren"],
"> PLZ's der Impfzentren: ",
lambda x: list(set([plz.strip() for plz in x.split(",")])))

if "code" not in kontaktdaten and command == "search":
kontaktdaten["code"] = input("> Code: ")
input_kontaktdaten_key(kontaktdaten, ["code"], "> Code: ")

if "kontakt" not in kontaktdaten:
kontaktdaten["kontakt"] = {}

if "anrede" not in kontaktdaten["kontakt"] and command == "search":
kontaktdaten["kontakt"]["anrede"] = input(
"> Anrede (Frau/Herr/...): ")
input_kontaktdaten_key(
kontaktdaten, ["kontakt", "anrede"], "> Anrede (Frau/Herr/...): ")

if "vorname" not in kontaktdaten["kontakt"] and command == "search":
kontaktdaten["kontakt"]["vorname"] = input("> Vorname: ")
input_kontaktdaten_key(
kontaktdaten, ["kontakt", "vorname"], "> Vorname: ")

if "nachname" not in kontaktdaten["kontakt"] and command == "search":
kontaktdaten["kontakt"]["nachname"] = input("> Nachname: ")
input_kontaktdaten_key(
kontaktdaten, ["kontakt", "nachname"], "> Nachname: ")

if "strasse" not in kontaktdaten["kontakt"] and command == "search":
kontaktdaten["kontakt"]["strasse"] = input("> Strasse: ")
input_kontaktdaten_key(
kontaktdaten, ["kontakt", "strasse"], "> Strasse: ")

if "hausnummer" not in kontaktdaten["kontakt"] and command == "search":
kontaktdaten["kontakt"]["hausnummer"] = input("> Hausnummer: ")
input_kontaktdaten_key(
kontaktdaten, ["kontakt", "hausnummer"], "> Hausnummer: ")

if "plz" not in kontaktdaten["kontakt"] and command == "search":
# Sicherstellen, dass die PLZ ein valides Format hat.
_wohnort_plz_valid = False
while not _wohnort_plz_valid:
wohnort_plz = input("> PLZ des Wohnorts: ")
wohnort_plz = wohnort_plz.strip()
if len(wohnort_plz) == 5 and wohnort_plz.isdigit():
_wohnort_plz_valid = True
else:
print(
f"Die eingegebene PLZ {wohnort_plz} scheint ungültig. Genau 5 Stellen und nur Ziffern sind erlaubt.")
kontaktdaten["kontakt"]["plz"] = wohnort_plz
input_kontaktdaten_key(
kontaktdaten, ["kontakt", "plz"], "> PLZ des Wohnorts: ")

if "ort" not in kontaktdaten["kontakt"] and command == "search":
kontaktdaten["kontakt"]["ort"] = input("> Wohnort: ")
input_kontaktdaten_key(
kontaktdaten, ["kontakt", "ort"], "> Wohnort: ")

if "phone" not in kontaktdaten["kontakt"]:
telefonnummer = input("> Telefonnummer: +49")
# Anführende Zahlen und Leerzeichen entfernen
telefonnummer = telefonnummer.strip()
telefonnummer = remove_prefix(telefonnummer, "+49")
telefonnummer = remove_prefix(telefonnummer, "0")
kontaktdaten["kontakt"]["phone"] = f"+49{telefonnummer}"
input_kontaktdaten_key(
kontaktdaten,
["kontakt", "phone"],
"> Telefonnummer: +49",
lambda x: x if x.startswith("+49") else f"+49{remove_prefix(x, '0')}")

if "notificationChannel" not in kontaktdaten["kontakt"]:
kontaktdaten["kontakt"]["notificationChannel"] = "email"

if "notificationReceiver" not in kontaktdaten["kontakt"]:
kontaktdaten["kontakt"]["notificationReceiver"] = input("> Mail: ")
input_kontaktdaten_key(
kontaktdaten, ["kontakt", "notificationReceiver"], "> Mail: ")

json.dump(kontaktdaten, file, ensure_ascii=False, indent=4)

return kontaktdaten


def get_kontaktdaten(filepath=None):
"""
Lade Kontaktdaten aus Datei.
:param filepath: Pfad zur JSON-Datei mit Kontaktdaten. Default: data/kontaktdaten.json im aktuellen Ordner
:return: Dictionary mit Kontaktdaten
"""

if filepath is None:
filepath = os.path.join(PATH, "data/kontaktdaten.json")

try:
with open(filepath, encoding='utf-8') as f:
try:
return json.load(f)
except json.JSONDecodeError:
return {}
except FileNotFoundError:
return {}
def input_kontaktdaten_key(
kontaktdaten,
path,
prompt,
transformer=lambda x: x):
target = kontaktdaten
for key in path[:-1]:
target = target[key]
key = path[-1]
while True:
target[key] = transformer(input(prompt).strip())
try:
validate_kontaktdaten(kontaktdaten)
break
except ValidationError as exc:
print(f"\n{str(exc)}\n")


def run_search_interactive(kontaktdaten_path, check_delay):
Expand All @@ -142,9 +137,6 @@ def run_search_interactive(kontaktdaten_path, check_delay):
:param kontaktdaten_path: Pfad zur JSON-Datei mit Kontaktdaten. Default: data/kontaktdaten.json im aktuellen Ordner
"""

if kontaktdaten_path is None:
kontaktdaten_path = os.path.join(PATH, "data/kontaktdaten.json")

print(
"Bitte trage zunächst deinen Impfcode und deine Kontaktdaten ein.\n"
f"Die Daten werden anschließend lokal in der Datei '{os.path.basename(kontaktdaten_path)}' abgelegt.\n"
Expand Down Expand Up @@ -209,9 +201,6 @@ def gen_code_interactive(kontaktdaten_path):
:param kontaktdaten_path: Pfad zur JSON-Datei mit Kontaktdaten. Default: kontaktdaten.json im aktuellen Ordner
"""

if kontaktdaten_path is None:
kontaktdaten_path = os.path.join(PATH, "data/kontaktdaten.json")

print(
"Du kannst dir jetzt direkt einen Impf-Code erstellen.\n"
"Dazu benötigst du eine Mailadresse, Telefonnummer und die PLZ deines Impfzentrums.\n"
Expand Down Expand Up @@ -243,15 +232,13 @@ def gen_code(kontaktdaten):
plz_impfzentrum = kontaktdaten["plz_impfzentren"][0]
mail = kontaktdaten["kontakt"]["notificationReceiver"]
telefonnummer = kontaktdaten["kontakt"]["phone"]
telefonnummer = telefonnummer.strip()
telefonnummer = remove_prefix(telefonnummer, "+49")
telefonnummer = remove_prefix(telefonnummer, "0")
if not telefonnummer.startswith("+49"):
telefonnummer = f"+49{remove_prefix(telefonnummer, '0')}"
except KeyError as exc:
print(
raise ValueError(
"Kontaktdaten konnten nicht aus 'kontaktdaten.json' geladen werden.\n"
"Bitte überprüfe, ob sie im korrekten JSON-Format sind oder gebe "
"deine Daten beim Programmstart erneut ein.\n")
raise exc
"deine Daten beim Programmstart erneut ein.\n") from exc

its = ImpfterminService("PLAT-ZHAL-TER1", [plz_impfzentrum], {},PATH)

Expand Down Expand Up @@ -355,8 +342,8 @@ def main():

args = parser.parse_args()

if not hasattr(args, "file"):
args.file = None
if not hasattr(args, "file") or args.file is None:
args.file = os.path.join(PATH, "data/kontaktdaten.json")
if not hasattr(args, "configure_only"):
args.configure_only = False
if not hasattr(args, "read_only"):
Expand All @@ -370,11 +357,16 @@ def main():
parser.error(str(exc))
# parser.error terminates the program with status code 2.

if args.command == "search":
subcommand_search(args)

elif args.command == "code":
subcommand_code(args)
if args.command is not None:
try:
if args.command == "search":
subcommand_search(args)
elif args.command == "code":
subcommand_code(args)
else:
assert False
except ValidationError as exc:
print(f"Fehler in {json.dumps(args.file)}:\n{str(exc)}")

else:
extended_settings = False
Expand Down Expand Up @@ -422,7 +414,7 @@ def main():
print("Falscheingabe! Bitte erneut versuchen.")
print()
except Exception as exc:
print(f"\nFehler: {str(exc)}\n")
print(f"\nFehler:\n{str(exc)}\n")


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions tools/its.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ def code_anfordern(self, mail, telefonnummer, plz_impfzentrum, leistungsmerkmal)
SMS-Code beim Impfterminservice anfordern.
:param mail: Mail für Empfang des Codes
:param telefonnummer: Telefonnummer für SMS-Code
:param telefonnummer: Telefonnummer für SMS-Code, inkl. Präfix +49
:param plz_impfzentrum: PLZ des Impfzentrums, für das ein Code erstellt werden soll
:param leistungsmerkmal: gewählte Impfgruppe (bspw. L921)
:return:
Expand All @@ -651,7 +651,7 @@ def code_anfordern(self, mail, telefonnummer, plz_impfzentrum, leistungsmerkmal)
data = {
"email": mail,
"leistungsmerkmal": leistungsmerkmal,
"phone": "+49" + telefonnummer,
"phone": telefonnummer,
"plz": plz_impfzentrum
}
while True:
Expand Down
135 changes: 135 additions & 0 deletions tools/kontaktdaten.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import json
import os
import re

from email.utils import parseaddr


class ValidationError(Exception):
pass


def get_kontaktdaten(filepath):
"""
Lade Kontaktdaten aus Datei.
:param filepath: Pfad zur JSON-Datei mit Kontaktdaten.
:return: Dictionary mit Kontaktdaten
"""

try:
with open(filepath, encoding='utf-8') as f:
try:
kontaktdaten = json.load(f)
validate_kontaktdaten(kontaktdaten)
return kontaktdaten
except json.JSONDecodeError:
return {}
except FileNotFoundError:
return {}


def validate_kontaktdaten(kontaktdaten):
"""
Erhebt ValidationError, falls Kontaktdaten ungültig sind.
Unvollständige Kontaktdaten sind ok und führen hier nicht zum Fehler.
:param kontaktdaten: Dictionary mit Kontaktdaten
"""

if not isinstance(kontaktdaten, dict):
raise ValidationError("Muss ein Dictionary sein")

for key, value in kontaktdaten.items():
try:
if key == "code":
validate_code(value)
elif key == "plz_impfzentren":
validate_plz_impfzentren(value)
elif key == "kontakt":
validate_kontakt(value)
else:
raise ValidationError(f"Nicht unterstützter Key")
except ValidationError as exc:
raise ValidationError(
f"Ungültiger Key {json.dumps(key)}:\n{str(exc)}")


def validate_code(code):
if not isinstance(code, str):
raise ValidationError("Muss eine Zeichenkette sein")

c = "[0-9a-zA-Z]"
if not re.match(f"^{4 * c}-{4 * c}-{4 * c}$", code):
raise ValidationError(
f"{json.dumps(code)} entspricht nicht dem Schema \"XXXX-XXXX-XXXX\"")


def validate_plz_impfzentren(plz_impfzentren):
if not isinstance(plz_impfzentren, list):
raise ValidationError("Muss eine Liste sein")

for plz in plz_impfzentren:
validate_plz(plz)


def validate_plz(plz):
if not isinstance(plz, str):
raise ValidationError("Muss eine Zeichenkette sein")

if not re.match(f"^{5 * '[0-9]'}$", plz):
raise ValidationError(
f"Ungültige PLZ {json.dumps(plz)} - muss aus genau 5 Ziffern bestehen")


def validate_kontakt(kontakt):
if not isinstance(kontakt, dict):
raise ValidationError("Muss ein Dictionary sein")

for key, value in kontakt.items():
try:
if key in ["anrede", "vorname", "nachname", "strasse", "ort"]:
if not isinstance(value, str):
raise ValidationError("Muss eine Zeichenkette sein")
elif key == "plz":
validate_plz(value)
elif key == "hausnummer":
validate_hausnummer(value)
elif key == "phone":
validate_phone(value)
elif key == "notificationChannel":
if value != "email":
raise ValidationError("Muss auf \"email\" gesetzt werden")
elif key == "notificationReceiver":
validate_email(value)
else:
raise ValidationError(f"Nicht unterstützter Key")
except ValidationError as exc:
raise ValidationError(
f"Ungültiger Key {json.dumps(key)}:\n{str(exc)}")


def validate_phone(phone):
if not isinstance(phone, str):
raise ValidationError("Muss eine Zeichenkette sein")

if not re.match(r"^(\+49)?[0-9]+$", phone):
raise ValidationError(
f"Ungültige Telefonnummer {json.dumps(phone)} - darf nur aus Ziffern bestehen")


def validate_hausnummer(hausnummer):
if not isinstance(hausnummer, str):
raise ValidationError("Muss eine Zeichenkette sein")

if len(hausnummer) > 20:
raise ValidationError(
f"Hausnummer {json.dumps(hausnummer)} ist zu lang - maximal 20 Zeichen erlaubt")


def validate_email(email):
if not isinstance(email, str):
raise ValidationError("Muss eine Zeichenkette sein")

if '@' not in parseaddr(email)[1]:
raise ValidationError(f"Ungültige E-Mail-Adresse {json.dumps(email)}")

0 comments on commit 2017cac

Please sign in to comment.