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>
This commit is contained in:
+142
-97
@@ -11,11 +11,12 @@ __license__ = "Python"
|
||||
|
||||
from itertools import chain
|
||||
from random import *
|
||||
import ast
|
||||
import configparser, os
|
||||
import copy
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from functools import total_ordering
|
||||
from xml.dom import minidom
|
||||
|
||||
|
||||
# Strukturklassen: Person, Gruppe, Tisch
|
||||
@@ -256,12 +257,15 @@ class Tische:
|
||||
config.read( os.path.expanduser(FileName) )
|
||||
for section in config.sections():
|
||||
Tid = int(section)
|
||||
Nummer = eval(config.get( section, 'Nummer' ))
|
||||
nPl = eval(config.get( section, 'Plaetze' ))
|
||||
Koords = eval(config.get( section, 'Koordinaten' ))
|
||||
Nachbarn = eval(config.get( section, 'Nachbarliste' ))
|
||||
Hofid = eval(config.get( section, 'Hof' ))
|
||||
T = Tisch( Tid, Plaetze=nPl, Koordinaten=Koords, Nachbarliste=Nachbarn, Hof=Hofid, Nummer=Nummer )
|
||||
# ast.literal_eval statt eval: tische.ini kann eine hochgeladene
|
||||
# Fremddatei sein (Web-M1), da darf kein beliebiger Code laufen.
|
||||
# Erlaubt weiterhin Zahlen, Tupel und Listen wie zuvor.
|
||||
Nummer = ast.literal_eval(config.get( section, 'Nummer' ))
|
||||
nPl = ast.literal_eval(config.get( section, 'Plaetze' ))
|
||||
Koords = ast.literal_eval(config.get( section, 'Koordinaten' ))
|
||||
Nachbarn = ast.literal_eval(config.get( section, 'Nachbarliste' ))
|
||||
Hofid = ast.literal_eval(config.get( section, 'Hof' ))
|
||||
T = Tisch( Tid, Plaetze=nPl, Koordinaten=Koords, Nachbarliste=Nachbarn, Hof=Hofid, Nummer=Nummer )
|
||||
self.Tisch_dazu( T )
|
||||
def vonId(self, SearchedId):
|
||||
for T in chain(self.TischListe):
|
||||
@@ -281,19 +285,21 @@ class Tische:
|
||||
class Plaetze:
|
||||
"""Informationen ueber freie oder benachbarte Plaetze"""
|
||||
def __init__(self, Tische):
|
||||
# mache Listen mit den Tischen mit n Freien Plaetzen
|
||||
self.__TIDsNFreie = []
|
||||
# je Anzahl freier Plaetze ein Set von Tisch-Ids. defaultdict(set)
|
||||
# erspart die manuelle Vorbelegung und liefert fuer noch nie
|
||||
# gesehene Fuellstaende automatisch ein leeres Set.
|
||||
self.__TIDsNFreie = defaultdict(set)
|
||||
self.__Groesster = Tische.Groesster
|
||||
self.__Tische = Tische
|
||||
for i in range( self.__Groesster + 1 ):
|
||||
self.__TIDsNFreie.append( set() )
|
||||
for T in Tische.TischListe:
|
||||
n_freie = T.freiePlaetze()
|
||||
self.__TIDsNFreie[n_freie].add(T.Id)
|
||||
def __repr__(self):
|
||||
result = "\n-- Tische mit n Freien Plaetzen:\n"
|
||||
for i in range(self.__Groesster+1):
|
||||
result = result + " " +repr(i) + " :" + repr(self.__TIDsNFreie[i]) + "\n"
|
||||
# .get statt [i], damit das Anzeigen keine leeren Eintraege
|
||||
# ins defaultdict schreibt
|
||||
result = result + " " +repr(i) + " :" + repr(self.__TIDsNFreie.get(i, set())) + "\n"
|
||||
return result
|
||||
def IdsnFreie(self, n_freie):
|
||||
"""gibt ein Set mit allen Tischids zurueck, wo noch X freie Plaetze sind"""
|
||||
@@ -406,11 +412,13 @@ class Strafliste:
|
||||
print("-- Strafliste laden")
|
||||
config = configparser.ConfigParser()
|
||||
config.read( os.path.expanduser(FileName) )
|
||||
self.NichtNachbar = eval(config.get( 'Globals', 'NichtNachbar' ))
|
||||
self.Allein = eval(config.get( 'Globals', 'Allein' ))
|
||||
self.TischWert = eval( config.get( 'Globals', 'TischWertigkeiten' ) )
|
||||
# ast.literal_eval statt eval: die Werte sind reine Literale
|
||||
# (Zahlen und Dicts wie {2:-10}), also kein Code noetig.
|
||||
self.NichtNachbar = ast.literal_eval(config.get( 'Globals', 'NichtNachbar' ))
|
||||
self.Allein = ast.literal_eval(config.get( 'Globals', 'Allein' ))
|
||||
self.TischWert = ast.literal_eval( config.get( 'Globals', 'TischWertigkeiten' ) )
|
||||
for (k,v) in config.items( 'Trennung' ):
|
||||
self.GruppenTrennen[int(k)] = eval(v)
|
||||
self.GruppenTrennen[int(k)] = ast.literal_eval(v)
|
||||
|
||||
|
||||
# Sitzplatzverteilung, bestehend aus den Einzelelementen
|
||||
@@ -618,10 +626,6 @@ class Sitzplatzverteilung:
|
||||
Teile.append(
|
||||
'<circle cx="%g" cy="%g" r="%g" fill="white" '
|
||||
'stroke="black" stroke-width="1.5"/>\n' % (Mx, My, TR) )
|
||||
Teile.append(
|
||||
'<text x="%g" y="%g" text-anchor="middle" '
|
||||
'font-size="10" fill="black">Tisch %s</text>\n'
|
||||
% (Mx, My - TR - 2, T.Id) )
|
||||
|
||||
Plaetze = list(T)
|
||||
n = len(Plaetze)
|
||||
@@ -636,6 +640,19 @@ class Sitzplatzverteilung:
|
||||
'stroke="black" stroke-width="1"/>\n'
|
||||
% (Px, Py, PersonenRadius, Farbe) )
|
||||
|
||||
# Tischnummer zentriert in die Tischmitte (nach den Personen-
|
||||
# kreisen gezeichnet, damit sie oben liegt; ein halbtransparenter
|
||||
# weisser Kreis darunter haelt sie auch ueber Personenkreisen
|
||||
# lesbar, falls die Tischmitte belegt sein sollte)
|
||||
Teile.append(
|
||||
'<circle cx="%g" cy="%g" r="%g" fill="white" '
|
||||
'fill-opacity="0.75"/>\n' % (Mx, My, PersonenRadius) )
|
||||
Teile.append(
|
||||
'<text x="%g" y="%g" text-anchor="middle" '
|
||||
'dominant-baseline="central" font-size="%g" '
|
||||
'font-weight="bold" fill="black">%s</text>\n'
|
||||
% (Mx, My, PersonenRadius * 1.1, T.Nummer) )
|
||||
|
||||
Teile.append( '</svg>\n' )
|
||||
|
||||
fout = open( OutputSVG, 'w' )
|
||||
@@ -897,11 +914,19 @@ class Sitzplatzverteilung:
|
||||
Tisch = Tische.vonId(TischId)
|
||||
self.PersonSetzen( Person, Tisch )
|
||||
def TopX(self, Liste, Anzahl=-1 ):
|
||||
"""gibt eine Liste mit X Ids zurueck, die die schlechtesten Wertungen haben"""
|
||||
"""gibt eine Liste mit X Ids zurueck, die die schlechtesten Wertungen haben
|
||||
|
||||
Achtung: Dies ist bewusst *nicht* heapq.nsmallest. Der laufende
|
||||
Schwellwert Min sinkt monoton mit, so dass nur Ids aufgenommen
|
||||
werden, die waehrend der Iteration einen neuen Tiefpunkt erreichen
|
||||
oder gleichziehen. Das Ergebnis haengt an der Iterationsreihenfolge
|
||||
und praegt das Mutationsverhalten des GA (mutieren() ->
|
||||
SchlechteGruppenTopX). Eine Umstellung auf heapq wuerde die
|
||||
Loesungsfindung veraendern und ist daher bewusst unterlassen.
|
||||
"""
|
||||
Min = 0
|
||||
RListe=[]
|
||||
Wertigkeit=[]
|
||||
# TODO filter verwenden
|
||||
for Id, Wert in Liste.items():
|
||||
if Wert <= Min:
|
||||
RListe.append(Id)
|
||||
@@ -911,92 +936,112 @@ class Sitzplatzverteilung:
|
||||
|
||||
|
||||
# Sitzplatzverteilung, bestehend aus den Einzelelementen
|
||||
class XMLConfig:
|
||||
"""aus Xml file alles einlesen"""
|
||||
def getText(self, nodelist):
|
||||
rc = ""
|
||||
for node in nodelist:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
rc = rc + node.data
|
||||
return rc
|
||||
def handlePersonNode( self, person ):
|
||||
"""Personendaten erfassen"""
|
||||
info = {}
|
||||
try:
|
||||
Titel = person.getElementsByTagName("Titel")[0]
|
||||
info["Titel"] = self.getText(Titel.childNodes)
|
||||
except IndexError:
|
||||
info["Titel"] = ""
|
||||
Vorname = person.getElementsByTagName("Vorname")[0]
|
||||
Nachname = person.getElementsByTagName("Nachname")[0]
|
||||
info["Vorname"] = self.getText(Vorname.childNodes)
|
||||
info["Nachname"] = self.getText(Nachname.childNodes)
|
||||
return info
|
||||
def HandleBestellung(self, Bestellung):
|
||||
"""alle Bloecke in der XML Bestellung parsen"""
|
||||
PidListe = []
|
||||
VipHash = {}
|
||||
class JSONConfig:
|
||||
"""liest die Gaesteliste aus einer bestellung.json ein
|
||||
(Nachfolger des frueheren XMLConfig; JSON hat die alte XML-Form
|
||||
abgeloest, siehe doc/dateiformate.md).
|
||||
|
||||
Erwartete Struktur:
|
||||
{
|
||||
"gruppen": [
|
||||
{
|
||||
"name": "Familie Huber", # optional, nur Anzeige
|
||||
"tisch": 1, # optional -> VIP-Fixplatz
|
||||
"anzahl": 1, # optional -> Sammelbuchung
|
||||
"personen": [
|
||||
{"vorname": "Anna", "nachname": "Huber", "titel": ""}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"tisch" macht die ganze Gruppe zu VIPs an diesem Tisch (laeuft
|
||||
nicht durch die GA-Platzierung). "anzahl" > 1 vervielfacht jede
|
||||
Person der Gruppe in so viele identische Buchungen (anonyme
|
||||
Sammelbuchung). "titel" ist optional.
|
||||
"""
|
||||
def HandleBestellung(self, Daten):
|
||||
"""die geladene JSON-Struktur in Personen/Gruppen/VIPs zerlegen
|
||||
|
||||
Rueckgabe: [ PN, GN, VIPGruppe, VipHash, GruppenNamen ]
|
||||
(wie CSVConfig; das fruehere XMLConfig gab die ersten vier
|
||||
Elemente zurueck - Aufrufer, die GruppenNamen nicht brauchen,
|
||||
koennen es ignorieren.)
|
||||
"""
|
||||
if not isinstance( Daten, dict ) or 'gruppen' not in Daten:
|
||||
raise ValueError("bestellung.json braucht ein Objekt mit dem "
|
||||
"Schluessel 'gruppen'")
|
||||
|
||||
PN = Personen()
|
||||
GN = Gruppen()
|
||||
VIPGruppe = Gruppe( 0 )
|
||||
VipHash = {}
|
||||
GruppenNamen = {}
|
||||
|
||||
print('-HandleBestellung')
|
||||
AlleGruppenXML = Bestellung.getElementsByTagName("Gruppe")
|
||||
PID = 0
|
||||
GID = 0
|
||||
for GruppeXML in AlleGruppenXML:
|
||||
GID = GID + 1
|
||||
print('-- GID ' + repr(GID))
|
||||
G = Gruppe( GID )
|
||||
for Index, GruppeJSON in enumerate( Daten['gruppen'] ):
|
||||
if 'personen' not in GruppeJSON:
|
||||
raise ValueError("Gruppe %d: 'personen' fehlt" % (Index + 1))
|
||||
|
||||
IsVip = False
|
||||
AnzahlReservierungen = 1
|
||||
for Node in GruppeXML.childNodes:
|
||||
if Node.nodeName == 'Tisch':
|
||||
TischNr = self.getText(Node.childNodes)
|
||||
print('TischNr:', TischNr)
|
||||
IsVip = True
|
||||
if Node.nodeName == 'Anzahl':
|
||||
AnzahlReservierungen = int(self.getText(Node.childNodes))
|
||||
print('Anzahl der Reservierungen:', AnzahlReservierungen)
|
||||
IsContingent = True
|
||||
IsVip = 'tisch' in GruppeJSON and GruppeJSON['tisch'] is not None
|
||||
if IsVip:
|
||||
try:
|
||||
TischNr = int( GruppeJSON['tisch'] )
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("Gruppe %d: 'tisch' ist keine "
|
||||
"Tischnummer" % (Index + 1))
|
||||
|
||||
for Node in GruppeXML.childNodes:
|
||||
if Node.nodeName == 'Person':
|
||||
NRes = 0
|
||||
for i in range(int(AnzahlReservierungen)):
|
||||
NRes = NRes + 1
|
||||
PID = PID + 1
|
||||
print("PID =" + repr(PID))
|
||||
info = self.handlePersonNode( Node )
|
||||
try:
|
||||
AnzahlReservierungen = int( GruppeJSON.get( 'anzahl', 1 ) )
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("Gruppe %d: 'anzahl' ist keine Zahl"
|
||||
% (Index + 1))
|
||||
if AnzahlReservierungen < 1:
|
||||
raise ValueError("Gruppe %d: 'anzahl' muss >= 1 sein"
|
||||
% (Index + 1))
|
||||
|
||||
# regulaere (nicht-VIP) Gruppen bekommen eine GID
|
||||
if not IsVip:
|
||||
GID = GID + 1
|
||||
G = Gruppe( GID )
|
||||
GruppenNamen[ GID ] = GruppeJSON.get( 'name', 'Gruppe %d' % GID )
|
||||
|
||||
for PersonJSON in GruppeJSON['personen']:
|
||||
Vorname = str( PersonJSON.get( 'vorname', '' ) ).strip()
|
||||
Nachname = str( PersonJSON.get( 'nachname', '' ) ).strip()
|
||||
Titel = str( PersonJSON.get( 'titel', '' ) ).strip()
|
||||
if Vorname == '' or Nachname == '':
|
||||
raise ValueError("Gruppe %d: jede Person braucht Vorname "
|
||||
"und Nachname" % (Index + 1))
|
||||
for i in range( AnzahlReservierungen ):
|
||||
PID = PID + 1
|
||||
P = Person( PID, Vorname, Nachname, Titel )
|
||||
if IsVip:
|
||||
VipHash[ PID ] = TischNr
|
||||
VIPGruppe.Person_dazu( P )
|
||||
else:
|
||||
PN.PersonAdd( P )
|
||||
G.Person_dazu( P )
|
||||
|
||||
if not IsVip:
|
||||
if G.Anzahl() > 0:
|
||||
GN.Gruppe_dazu( G )
|
||||
else:
|
||||
# leere Gruppe wieder verwerfen
|
||||
del GruppenNamen[ GID ]
|
||||
GID = GID - 1
|
||||
|
||||
return [ PN, GN, VIPGruppe, VipHash, GruppenNamen ]
|
||||
|
||||
Vorname = info["Vorname"]
|
||||
Nachname = info["Nachname"]
|
||||
Titel = info["Titel"]
|
||||
P = Person( PID, Vorname, Nachname, Titel)
|
||||
if IsVip == True:
|
||||
PidListe.append( int(PID) )
|
||||
VipHash[ int(PID) ] = int(TischNr)
|
||||
VIPGruppe.Person_dazu( P )
|
||||
else:
|
||||
PN.PersonAdd( P )
|
||||
G.Person_dazu( P )
|
||||
if (G.Anzahl() > 0) & (IsVip != True):
|
||||
GN.Gruppe_dazu( G )
|
||||
if IsVip == True:
|
||||
GID = GID - 1
|
||||
print(PN)
|
||||
print(GN)
|
||||
print(VIPGruppe)
|
||||
print(VipHash)
|
||||
print('==========')
|
||||
return [ PN, GN, VIPGruppe, VipHash ]
|
||||
def laden(self, Bestellung):
|
||||
"""lese alle Eingangsdaten aus einem XML File ein"""
|
||||
fsock = open( Bestellung, 'r')
|
||||
xmlini = minidom.parse(fsock)
|
||||
return self.HandleBestellung(xmlini)
|
||||
"""lese alle Eingangsdaten aus einer JSON-Datei ein"""
|
||||
import json
|
||||
# utf-8-sig schluckt ein evtl. vorhandenes BOM
|
||||
fsock = open( Bestellung, 'r', encoding='utf-8-sig' )
|
||||
Daten = json.load( fsock )
|
||||
fsock.close()
|
||||
return self.HandleBestellung( Daten )
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user