Files
platz/libs/CSVConfig.py
T
Michael Stangl 9de80d2f2a Web-M1-Ausbau: JSON-Gaesteliste, GA-Verlauf, Python-Serversteuerung, tische.ini-Upload
- bestellung.xml -> bestellung.json: neuer JSONConfig (loest XMLConfig ab),
  alle work-Beispiele konvertiert, Doku (CLAUDE/README/dateiformate) umgestellt
- ga.py: Beobachtungsobjekt Entwicklung zeichnet den Loesungsverlauf auf,
  neuer Schalter --show-development erzeugt Entwicklung.svg (beste/mittlere/
  schlechteste Wertigkeit ueber die Zyklus-Aktionen)
- web/main.py: neuer Endpunkt /tische-datei (tische.ini-Upload, ast.literal_eval),
  Frontend kann jetzt eine tische.ini laden
- libs/webserver.py: plattformunabhaengige start/stop/status-Steuerung der
  Web-Testinstanz (ersetzt run_web_helper.ps1), raeumt auch verwaiste
  --reload-Prozessbaeume zuverlaessig auf
- Sicherheit/Modernisierung: eval() -> ast.literal_eval in Tische/Strafliste,
  sicherer Mini-Auswerter fuer die String-Ausdruecke in zyklus.cfg,
  defaultdict(set) in Plaetze
- SVG: Tischnummer fett zentriert im Tischkreis statt "Tisch x" darueber
- Tests: JSONConfig, Entwicklung und der Zyklus-Auswerter abgedeckt (9 Tests)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 12:49:42 +02:00

191 lines
6.2 KiB
Python

"""
Liest eine Gaesteliste im CSV-Format ein — der Web-Ersatz fuer bestellung.json
(Meilenstein 1 der Roadmap, Adapter analog zu JSONConfig in Strukturdaten.py).
CSV-Format (Kopfzeile noetig, Spaltenreihenfolge egal, Trennzeichen ; oder ,):
Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl
- Vorname, Nachname: Pflicht
- Titel: optional (z.B. "Dr.")
- Gruppe: Gruppenname — Personen mit demselben Gruppennamen gehoeren
zusammen und sollen moeglichst zusammen sitzen.
Leer = Einzelperson (bildet eine eigene Ein-Personen-Gruppe).
- Tisch: optionale Tischnummer — die Person ist VIP und wird fest an
diesen Tisch gesetzt (wie "tisch" in bestellung.json).
VIPs laufen nicht durch die GA-Platzierung.
- Anzahl: optionale Reservierungsanzahl — expandiert die Zeile in so
viele identische Buchungen (wie "anzahl" in bestellung.json,
z.B. fuer anonyme Sammelbuchungen). Leer = 1.
Zusaetzlich werden die Spalten "MagNicht" und "MagGern" akzeptiert,
aber noch ignoriert (reserviert fuer Personenpaar-Regeln, siehe
Roadmap-Konzeptentscheidung Nr. 10) — so entsteht spaeter kein
Formatbruch.
"""
__author__ = "Michael Stangl"
__copyright__ = "Copyright (c) 2026 Michael Stangl"
__license__ = "Python"
import csv
import io
from Strukturdaten import Person, Personen, Gruppe, Gruppen
class CSVConfig:
"""aus CSV Text/Datei alles einlesen (Rueckgabe wie JSONConfig.laden,
plus Gruppennamen fuer die Anzeige im Web-Frontend)"""
PflichtSpalten = ['vorname', 'nachname']
BekannteSpalten = ['vorname', 'nachname', 'titel', 'gruppe',
'tisch', 'anzahl', 'magnicht', 'maggern']
def HandleGaesteliste(self, Text):
"""parst den CSV-Text und baut Personen/Gruppen/VIPs auf
Rueckgabe: [ PN, GN, VIPGruppe, VipHash, GruppenNamen ]
- PN: Personen-Container (ohne VIPs, wie JSONConfig)
- GN: Gruppen-Container (ohne VIP-Gruppe, wie JSONConfig)
- VIPGruppe: Gruppe(0) mit allen fest gesetzten Personen
- VipHash: { PersonId: Tischnummer } fuer VIPsSetzen()
- GruppenNamen: { GruppenId: Anzeigename }
"""
# Trennzeichen automatisch bestimmen: die Kopfzeile entscheidet
Kopfzeile = Text.lstrip('').splitlines()[0] if Text.strip() else ''
Trenner = ';' if Kopfzeile.count(';') >= Kopfzeile.count(',') else ','
Leser = csv.DictReader( io.StringIO(Text.lstrip('')), delimiter=Trenner )
if Leser.fieldnames is None:
raise ValueError("CSV ist leer — Kopfzeile mit mindestens "
"Vorname/Nachname erwartet")
# Spaltennamen normalisieren (Gross/Klein egal, Leerzeichen weg)
SpaltenMap = {}
for Feld in Leser.fieldnames:
if Feld is None:
continue
SpaltenMap[Feld] = Feld.strip().lower()
for Pflicht in self.PflichtSpalten:
if Pflicht not in SpaltenMap.values():
raise ValueError("CSV-Kopfzeile: Pflichtspalte '%s' fehlt "
"(gefunden: %s)" % (Pflicht, Leser.fieldnames))
def Wert(Zeile, Name):
for Original, Normiert in SpaltenMap.items():
if Normiert == Name:
Inhalt = Zeile.get(Original)
return (Inhalt or '').strip()
return ''
PN = Personen()
GN = Gruppen()
VIPGruppe = Gruppe( 0 )
VipHash = {}
GruppenNamen = {}
# Gruppen entstehen beim ersten Auftreten ihres Namens;
# Einzelpersonen (leerer Gruppenname) bekommen eine eigene Gruppe
GruppeVonName = {}
PID = 0
GID = 0
ZeilenNr = 1
for Zeile in Leser:
ZeilenNr = ZeilenNr + 1
Vorname = Wert(Zeile, 'vorname')
Nachname = Wert(Zeile, 'nachname')
if Vorname == '' and Nachname == '':
continue # Leerzeile
if Vorname == '' or Nachname == '':
raise ValueError("Zeile %d: Vorname und Nachname sind Pflicht"
% ZeilenNr)
Titel = Wert(Zeile, 'titel')
GruppenName = Wert(Zeile, 'gruppe')
TischFeld = Wert(Zeile, 'tisch')
AnzahlFeld = Wert(Zeile, 'anzahl')
try:
AnzahlReservierungen = int(AnzahlFeld) if AnzahlFeld else 1
except ValueError:
raise ValueError("Zeile %d: Anzahl '%s' ist keine Zahl"
% (ZeilenNr, AnzahlFeld))
if AnzahlReservierungen < 1:
raise ValueError("Zeile %d: Anzahl muss >= 1 sein" % ZeilenNr)
TischNr = None
if TischFeld != '':
try:
TischNr = int(TischFeld)
except ValueError:
raise ValueError("Zeile %d: Tisch '%s' ist keine "
"Tischnummer" % (ZeilenNr, TischFeld))
for i in range(AnzahlReservierungen):
PID = PID + 1
P = Person( PID, Vorname, Nachname, Titel )
if TischNr is not None:
# VIP: fester Tisch, keine GA-Platzierung
VipHash[ PID ] = TischNr
VIPGruppe.Person_dazu( P )
else:
PN.PersonAdd( P )
if GruppenName == '':
# Einzelperson: eigene Ein-Personen-Gruppe,
# komplett befuellen und dann registrieren
GID = GID + 1
G = Gruppe( GID )
G.Person_dazu( P )
GruppenNamen[ GID ] = '%s %s' % (Vorname, Nachname)
GN.Gruppe_dazu( G )
else:
if GruppenName not in GruppeVonName:
GID = GID + 1
G = Gruppe( GID )
GruppeVonName[ GruppenName ] = G
GruppenNamen[ GID ] = GruppenName
else:
G = GruppeVonName[ GruppenName ]
G.Person_dazu( P )
# benannte Gruppen erst jetzt registrieren, wenn sie komplett sind
# (Gruppen.Gruppe_dazu uebernimmt Mitglieder und zaehlt NLeute)
for GruppenName, G in GruppeVonName.items():
if G.Anzahl() > 0:
GN.Gruppe_dazu( G )
return [ PN, GN, VIPGruppe, VipHash, GruppenNamen ]
def laden(self, FileName):
"""lese alle Eingangsdaten aus einer CSV-Datei ein"""
# utf-8-sig schluckt das BOM, das Excel beim CSV-Export voranstellt
fsock = open( FileName, 'r', encoding='utf-8-sig' )
Text = fsock.read()
fsock.close()
return self.HandleGaesteliste( Text )
if __name__ == '__main__':
Beispiel = """Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl
Britta;Beispiel;;Brautpaar;1;
Bernd;Beispiel;Dr.;Brautpaar;1;
Anna;Huber;;Familie Huber;;
Berta;Huber;;Familie Huber;;
Carl;Huber;;Familie Huber;;
Gast;Kollege;;Kollegen;;4
Einzel;Gast;;;;
"""
print("\n-- Parse Beispiel-CSV:")
C = CSVConfig()
[ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = C.HandleGaesteliste( Beispiel )
print(PN)
print(GN)
print(VIPGruppe)
print(VipHash)
print(GruppenNamen)
else:
pass