535 lines
18 KiB
Python
535 lines
18 KiB
Python
"""
|
|
REST-API um den bestehenden Sitzplatz-Kern (Meilenstein 1 der Roadmap).
|
|
|
|
Session-basiert, nichts wird dauerhaft gespeichert: jede Sitzung lebt
|
|
im Speicher des Servers und verfaellt nach 24 Stunden (TTL) — bewusst
|
|
ohne Accounts/DB, siehe Roadmap M1 ("schont Datenschutz-Aufwand").
|
|
|
|
Endpunkte (siehe auch /docs — interaktive Swagger-UI von FastAPI):
|
|
POST /sitzung — legt eine Arbeits-Session an
|
|
GET /sitzung/{id} — Status der Session
|
|
POST /sitzung/{id}/tische — Tisch-Layout als JSON (Ersatz tische.ini)
|
|
POST /sitzung/{id}/personen — Gaesteliste als CSV-Upload (Ersatz bestellung.json)
|
|
POST /sitzung/{id}/berechnen — startet die GA-Optimierung; liefert bis zu
|
|
?varianten=N unterschiedliche Loesungen (M2)
|
|
GET /sitzung/{id}/ergebnis — alle Varianten des letzten Laufs als JSON
|
|
GET /sitzung/{id}/ergebnis.svg — eine Variante als SVG (?variante=k, 1 = beste)
|
|
|
|
Start (lokale Testinstanz, siehe bin/run_web.bat / bin/run_web.sh):
|
|
python -m uvicorn main:app --reload --app-dir web
|
|
"""
|
|
|
|
import ast
|
|
import configparser
|
|
import math
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import uuid
|
|
|
|
# Projektwurzel und Pfade robust aus der eigenen Lage ableiten, damit der
|
|
# Server auch ohne vorheriges setenv laeuft (PLATZ_* hat Vorrang, falls gesetzt)
|
|
BASISDIR = os.path.dirname( os.path.dirname( os.path.abspath( __file__ ) ) )
|
|
LIBSDIR = os.environ.get( 'PLATZ_LIBS', os.path.join( BASISDIR, 'libs' ) )
|
|
CFGDIR = os.environ.get( 'PLATZ_CFG', os.path.join( BASISDIR, 'cfg' ) )
|
|
if LIBSDIR not in sys.path:
|
|
sys.path.insert( 0, LIBSDIR )
|
|
|
|
from fastapi import FastAPI, File, HTTPException, Query, UploadFile
|
|
from fastapi.responses import Response
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel, Field
|
|
|
|
from ga import Farm, Zyklus
|
|
from CSVConfig import CSVConfig
|
|
from Strukturdaten import Sitzplatzverteilung, Strafliste, Tisch, Tische
|
|
|
|
app = FastAPI(
|
|
title='Sitz & Platz — API',
|
|
description='Sitzplatz-Optimierung per genetischem Algorithmus '
|
|
'(Meilenstein 1: Session-basiert, ohne Accounts)',
|
|
version='0.1.0' )
|
|
|
|
# ---------------------------------------------------------------- GA-Konfig
|
|
# Zyklus und Strafliste einmal beim Start laden (wie in libs/platz.py):
|
|
# zyklus.cfg und strafen.cfg direkt aus dem cfg-Verzeichnis (PLATZ_CFG bzw.
|
|
# Projekt-cfg). GA-Profil per Umgebungsvariable PLATZ_ZYKLUSART (Vorgabe:
|
|
# Adaptiv), Strafpunkte-Profil per PLATZ_STRAFENART (Vorgabe: default) —
|
|
# analog zum Schalter --strafen in libs/platz.py.
|
|
ZYKLUSDATEI = os.path.join( CFGDIR, 'zyklus.cfg' )
|
|
STRAFENDATEI = os.path.join( CFGDIR, 'strafen.cfg' )
|
|
ZYKLUSART = os.environ.get( 'PLATZ_ZYKLUSART', 'Adaptiv' )
|
|
STRAFENART = os.environ.get( 'PLATZ_STRAFENART', 'default' )
|
|
|
|
GAZyklus = Zyklus()
|
|
GAZyklus.laden( ZYKLUSDATEI, ZYKLUSART )
|
|
Strafen = Strafliste()
|
|
Strafen.laden( STRAFENDATEI, STRAFENART )
|
|
|
|
# ---------------------------------------------------------------- Sessions
|
|
SESSION_TTL_SEKUNDEN = 24 * 3600
|
|
Sessions = {}
|
|
SessionsLock = threading.Lock()
|
|
|
|
|
|
def _SessionsAufraeumen():
|
|
"""Sessions loeschen, deren TTL abgelaufen ist (Datenschutz per Design)"""
|
|
Jetzt = time.time()
|
|
with SessionsLock:
|
|
Abgelaufen = [ Sid for Sid, S in Sessions.items()
|
|
if Jetzt - S['erzeugt'] > SESSION_TTL_SEKUNDEN ]
|
|
for Sid in Abgelaufen:
|
|
del Sessions[Sid]
|
|
|
|
|
|
def _GibSession(SitzungsId):
|
|
_SessionsAufraeumen()
|
|
with SessionsLock:
|
|
S = Sessions.get( SitzungsId )
|
|
if S is None:
|
|
raise HTTPException( status_code=404,
|
|
detail='Sitzung nicht gefunden oder abgelaufen (TTL 24h)' )
|
|
return S
|
|
|
|
|
|
# ------------------------------------------------------------ API-Modelle
|
|
class TischEingabe(BaseModel):
|
|
"""ein Tisch aus dem Frontend (Ersatz fuer eine tische.ini-Section)"""
|
|
id: int | None = Field( default=None, description='Tisch-Id; leer = fortlaufend' )
|
|
nummer: int | None = Field( default=None, description='Anzeigenummer; leer = wie Id' )
|
|
hof: int = Field( default=1, description='Hof/Raum-Nummer' )
|
|
plaetze: int = Field( gt=0, le=100, description='Anzahl Sitzplaetze' )
|
|
x: float = Field( description='X-Koordinate im Raster' )
|
|
y: float = Field( description='Y-Koordinate im Raster' )
|
|
nachbarn: list[int] | None = Field( default=None,
|
|
description='explizite Nachbar-Tisch-Ids; leer = aus Naehe ableiten' )
|
|
|
|
|
|
class TischLayout(BaseModel):
|
|
tische: list[TischEingabe] = Field( min_length=1 )
|
|
nachbarRadius: float = Field( default=1.5, gt=0,
|
|
description='max. Rasterabstand, bis zu dem zwei Tische als '
|
|
'Nachbarn gelten (nur fuer Tische ohne explizite Liste)' )
|
|
|
|
|
|
# --------------------------------------------------------------- Hilfen
|
|
def _NachbarnAbleiten(TischListe, Radius):
|
|
"""Nachbarliste aus raeumlicher Naehe der Koordinaten ableiten
|
|
(Roadmap M1: Ersatz fuer manuelle Nachbarliste-Pflege).
|
|
Explizit angegebene Nachbarn bleiben erhalten und werden
|
|
beidseitig ergaenzt."""
|
|
Nachbarn = { T.id: set( T.nachbarn or [] ) for T in TischListe }
|
|
for T1 in TischListe:
|
|
for T2 in TischListe:
|
|
if T1.id == T2.id:
|
|
continue
|
|
if T1.nachbarn is None or T2.nachbarn is None:
|
|
Abstand = math.dist( (T1.x, T1.y), (T2.x, T2.y) )
|
|
if Abstand <= Radius:
|
|
Nachbarn[T1.id].add( T2.id )
|
|
Nachbarn[T2.id].add( T1.id )
|
|
# expliziten Angaben beidseitig machen
|
|
for Tid, Menge in Nachbarn.items():
|
|
for NachbarId in Menge:
|
|
if NachbarId in Nachbarn:
|
|
Nachbarn[NachbarId].add( Tid )
|
|
return { Tid: sorted(Menge) for Tid, Menge in Nachbarn.items() }
|
|
|
|
|
|
def _TischeBauen(S):
|
|
"""baut aus dem gespeicherten Layout frische Tisch-Objekte
|
|
(pro Berechnung neu, damit keine Reste vorheriger Laeufe kleben)"""
|
|
TE = Tische( [] )
|
|
for T in S['tische']:
|
|
TE.Tisch_dazu( Tisch(
|
|
T['id'],
|
|
Plaetze=T['plaetze'],
|
|
Koordinaten=( T['x'], T['y'] ),
|
|
Nachbarliste=T['nachbarn'],
|
|
Hof=T['hof'],
|
|
Nummer=T['nummer'] ) )
|
|
return TE
|
|
|
|
|
|
def _Signatur(SV):
|
|
"""Sitzordnungs-Signatur einer Loesung: welche Personen an welchem Tisch.
|
|
Zwei Loesungen mit gleicher Signatur sind fuer den Nutzer dieselbe
|
|
Sitzordnung (der GA-Pool enthaelt viele Mutations-Verwandte) — die
|
|
Varianten-Auswahl dedupliziert damit (Roadmap M2: 'unterschiedliche
|
|
gute Loesungen')."""
|
|
return frozenset(
|
|
( T.Id, frozenset( P.Id for P in T ) ) for T in SV.Tische )
|
|
|
|
|
|
def _VarianteJSON(SV, GruppenNamen, Nr):
|
|
"""baut die JSON-Darstellung EINER Loesung (eine Variante)"""
|
|
def GruppeVonPerson(P):
|
|
try:
|
|
G = SV.Gruppen.GruppeVonPid( P.Id )
|
|
return GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id )
|
|
except KeyError:
|
|
return 'VIP'
|
|
|
|
TischListe = []
|
|
for T in SV.Tische:
|
|
Personen = [ {
|
|
'titel': P._Titel,
|
|
'vorname': P._Vorname,
|
|
'nachname': P._Name,
|
|
'gruppe': GruppeVonPerson( P ),
|
|
} for P in T ]
|
|
Personen.sort( key=lambda p: (p['gruppe'], p['nachname'], p['vorname']) )
|
|
TischListe.append( {
|
|
'id': T.Id,
|
|
'nummer': T.Nummer,
|
|
'hof': T.Hof,
|
|
'plaetze': T.Plaetze,
|
|
'besetzt': T.besetztePlaetze(),
|
|
'frei': T.freiePlaetze(),
|
|
'wert': SV.TischWertGet( T ),
|
|
'personen': Personen,
|
|
} )
|
|
TischListe.sort( key=lambda t: t['id'] )
|
|
|
|
GruppenListe = []
|
|
for G in SV.Gruppen:
|
|
GruppenListe.append( {
|
|
'id': G.Id,
|
|
'name': GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ),
|
|
'anzahl': G.Anzahl(),
|
|
'teilungen': SV.GruppenTeilungGet( G ),
|
|
'wert': SV.GruppenWertGet( G ),
|
|
} )
|
|
GruppenListe.sort( key=lambda g: g['id'] )
|
|
|
|
# Vergleichs-Kennzahlen (Roadmap M2: Wertigkeit, Gruppentrennungen,
|
|
# allein sitzende Personen — LeuteAllein zaehlt nur Personen aus
|
|
# mehrkoepfigen Gruppen ohne Gruppenmitglied am Tisch)
|
|
Kennzahlen = {
|
|
'wertigkeit': SV.value,
|
|
'getrennteGruppen': sum( 1 for G in GruppenListe if G['teilungen'] > 0 ),
|
|
'teilungenGesamt': sum( G['teilungen'] for G in GruppenListe ),
|
|
'alleinePersonen': sum( len( SV.LeuteAllein(T) ) for T in SV.Tische ),
|
|
}
|
|
|
|
return {
|
|
'nr': Nr,
|
|
'wertigkeit': SV.value,
|
|
'kennzahlen': Kennzahlen,
|
|
'tische': TischListe,
|
|
'gruppen': GruppenListe,
|
|
}
|
|
|
|
|
|
def _ErgebnisJSON(S):
|
|
"""baut die JSON-Darstellung aller Varianten des letzten Laufs"""
|
|
GruppenNamen = S.get( 'gruppenNamen', {} )
|
|
return {
|
|
'lauf': S['laeufe'],
|
|
'varianten': [ _VarianteJSON( SV, GruppenNamen, Nr + 1 )
|
|
for Nr, SV in enumerate( S['ergebnisse'] ) ],
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------------- Endpunkte
|
|
@app.post( '/sitzung', status_code=201 )
|
|
def SitzungAnlegen():
|
|
"""legt eine neue Arbeits-Session an (TTL 24h, nur im Speicher)"""
|
|
_SessionsAufraeumen()
|
|
SitzungsId = uuid.uuid4().hex
|
|
with SessionsLock:
|
|
Sessions[SitzungsId] = {
|
|
'erzeugt': time.time(),
|
|
'tische': None,
|
|
'personen': None,
|
|
'ergebnisse': [], # Varianten des letzten Laufs (beste zuerst)
|
|
'laeufe': 0, # wie oft .../berechnen aufgerufen wurde
|
|
}
|
|
return { 'id': SitzungsId, 'ttlStunden': SESSION_TTL_SEKUNDEN // 3600 }
|
|
|
|
|
|
@app.get( '/sitzung/{SitzungsId}' )
|
|
def SitzungStatus(SitzungsId: str):
|
|
"""Status: was ist schon da, was fehlt noch"""
|
|
S = _GibSession( SitzungsId )
|
|
return {
|
|
'id': SitzungsId,
|
|
'tische': len( S['tische'] ) if S['tische'] else 0,
|
|
'personen': S['personen']['nLeute'] if S['personen'] else 0,
|
|
'vips': S['personen']['nVips'] if S['personen'] else 0,
|
|
'berechnet': len( S['ergebnisse'] ) > 0,
|
|
'laeufe': S['laeufe'],
|
|
'varianten': len( S['ergebnisse'] ),
|
|
}
|
|
|
|
|
|
@app.post( '/sitzung/{SitzungsId}/tische' )
|
|
def TischeSetzen(SitzungsId: str, Layout: TischLayout):
|
|
"""Tisch-Layout uebernehmen (JSON-Ersatz fuer tische.ini);
|
|
Nachbarschaft wird aus raeumlicher Naehe abgeleitet"""
|
|
S = _GibSession( SitzungsId )
|
|
|
|
# Ids vergeben/pruefen
|
|
NaechsteId = 1
|
|
Vergeben = set()
|
|
for T in Layout.tische:
|
|
if T.id is None:
|
|
while NaechsteId in Vergeben:
|
|
NaechsteId = NaechsteId + 1
|
|
T.id = NaechsteId
|
|
if T.id in Vergeben:
|
|
raise HTTPException( status_code=400,
|
|
detail='Tisch-Id %d ist doppelt' % T.id )
|
|
Vergeben.add( T.id )
|
|
|
|
Nachbarn = _NachbarnAbleiten( Layout.tische, Layout.nachbarRadius )
|
|
|
|
S['tische'] = [ {
|
|
'id': T.id,
|
|
'nummer': T.nummer if T.nummer is not None else T.id,
|
|
'hof': T.hof,
|
|
'plaetze': T.plaetze,
|
|
'x': T.x,
|
|
'y': T.y,
|
|
'nachbarn': Nachbarn[T.id],
|
|
} for T in Layout.tische ]
|
|
# Nachbar-Radius fuer den Rueckweg merken (GET .../tische), damit der
|
|
# grafische Editor dasselbe Layout inkl. Radius wieder laden kann.
|
|
S['nachbarRadius'] = Layout.nachbarRadius
|
|
S['ergebnisse'] = []
|
|
|
|
return {
|
|
'tische': S['tische'],
|
|
'plaetzeGesamt': sum( T['plaetze'] for T in S['tische'] ),
|
|
}
|
|
|
|
|
|
@app.get( '/sitzung/{SitzungsId}/tische' )
|
|
def TischeHolen(SitzungsId: str):
|
|
"""das gespeicherte Tisch-Layout zurueckgeben (fuer den grafischen
|
|
Editor: Layout aus einer geteilten Session wieder aufnehmen).
|
|
Die abgeleitete Nachbarliste ist pro Tisch enthalten; der zuletzt
|
|
verwendete nachbarRadius kommt separat mit (Vorgabe 1.5)."""
|
|
S = _GibSession( SitzungsId )
|
|
return {
|
|
'tische': S['tische'] or [],
|
|
'nachbarRadius': S.get( 'nachbarRadius', 1.5 ),
|
|
}
|
|
|
|
|
|
@app.post( '/sitzung/{SitzungsId}/tische-datei' )
|
|
def TischDateiHochladen(SitzungsId: str, datei: UploadFile = File(...)):
|
|
"""bestehende tische.ini hochladen (gleiches Format wie fuer die CLI,
|
|
z.B. work/hochzeit/tische.ini); die Nachbarliste aus der Datei
|
|
wird unveraendert uebernommen, nichts wird neu abgeleitet"""
|
|
S = _GibSession( SitzungsId )
|
|
Rohdaten = datei.file.read()
|
|
try:
|
|
Text = Rohdaten.decode( 'utf-8-sig' )
|
|
except UnicodeDecodeError:
|
|
Text = Rohdaten.decode( 'cp1252' )
|
|
|
|
config = configparser.ConfigParser()
|
|
try:
|
|
config.read_string( Text )
|
|
except configparser.Error as Fehler:
|
|
raise HTTPException( status_code=400,
|
|
detail='tische.ini nicht lesbar: %s' % Fehler )
|
|
if not config.sections():
|
|
raise HTTPException( status_code=400,
|
|
detail='tische.ini enthaelt keine Tisch-Sektionen ([1], [2], ...)' )
|
|
|
|
# gleiche Feldsemantik wie Tische.laden() in Strukturdaten.py,
|
|
# aber mit ast.literal_eval statt eval (Upload = fremde Eingabe)
|
|
TischListe = []
|
|
for Sektion in config.sections():
|
|
try:
|
|
Tid = int( Sektion )
|
|
Plaetze = int( ast.literal_eval( config.get( Sektion, 'Plaetze' ) ) )
|
|
Koords = ast.literal_eval( config.get( Sektion, 'Koordinaten' ) )
|
|
Nachbarn = list( ast.literal_eval(
|
|
config.get( Sektion, 'Nachbarliste' ) ) )
|
|
Nummer = int( ast.literal_eval(
|
|
config.get( Sektion, 'Nummer', fallback=Sektion ) ) )
|
|
Hof = int( ast.literal_eval(
|
|
config.get( Sektion, 'Hof', fallback='1' ) ) )
|
|
except (ValueError, SyntaxError, configparser.Error) as Fehler:
|
|
raise HTTPException( status_code=400,
|
|
detail='tische.ini, Sektion [%s]: %s' % (Sektion, Fehler) )
|
|
if Plaetze < 1:
|
|
raise HTTPException( status_code=400,
|
|
detail='tische.ini, Sektion [%s]: Plaetze muss >= 1 sein'
|
|
% Sektion )
|
|
TischListe.append( {
|
|
'id': Tid,
|
|
'nummer': Nummer,
|
|
'hof': Hof,
|
|
'plaetze': Plaetze,
|
|
'x': float( Koords[0] ),
|
|
'y': float( Koords[1] ),
|
|
'nachbarn': sorted( int(N) for N in Nachbarn ),
|
|
} )
|
|
|
|
Ids = [ T['id'] for T in TischListe ]
|
|
if len( Ids ) != len( set( Ids ) ):
|
|
raise HTTPException( status_code=400,
|
|
detail='tische.ini enthaelt doppelte Tisch-Ids' )
|
|
|
|
S['tische'] = TischListe
|
|
S['ergebnisse'] = []
|
|
return {
|
|
'tische': S['tische'],
|
|
'plaetzeGesamt': sum( T['plaetze'] for T in S['tische'] ),
|
|
}
|
|
|
|
|
|
@app.post( '/sitzung/{SitzungsId}/personen' )
|
|
def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
|
|
"""Gaesteliste als CSV hochladen (Ersatz fuer bestellung.json);
|
|
Format siehe libs/CSVConfig.py"""
|
|
S = _GibSession( SitzungsId )
|
|
Rohdaten = datei.file.read()
|
|
try:
|
|
Text = Rohdaten.decode( 'utf-8-sig' )
|
|
except UnicodeDecodeError:
|
|
Text = Rohdaten.decode( 'cp1252' ) # Excel/Windows-Fallback
|
|
|
|
try:
|
|
[ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = \
|
|
CSVConfig().HandleGaesteliste( Text )
|
|
except ValueError as Fehler:
|
|
raise HTTPException( status_code=400, detail=str(Fehler) )
|
|
|
|
S['personen'] = {
|
|
'PN': PN,
|
|
'GN': GN,
|
|
'VIPGruppe': VIPGruppe,
|
|
'VipHash': VipHash,
|
|
'nLeute': GN.NLeute,
|
|
'nVips': VIPGruppe.Anzahl(),
|
|
}
|
|
S['gruppenNamen'] = GruppenNamen
|
|
S['ergebnisse'] = []
|
|
|
|
return {
|
|
'personen': GN.NLeute,
|
|
'vips': VIPGruppe.Anzahl(),
|
|
'gruppen': [ {
|
|
'id': G.Id,
|
|
'name': GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ),
|
|
'anzahl': G.Anzahl(),
|
|
} for G in GN ],
|
|
}
|
|
|
|
|
|
@app.post( '/sitzung/{SitzungsId}/berechnen' )
|
|
def Berechnen(SitzungsId: str,
|
|
varianten: int = Query( default=3, ge=1, le=5,
|
|
description='wieviele unterschiedliche Varianten der Lauf '
|
|
'zurueckgeben soll (aus Farm.TopN, dedupliziert)' )):
|
|
"""startet die GA-Optimierung; jeder Aufruf ist ein neuer Lauf und
|
|
liefert bis zu 'varianten' unterschiedliche gute Sitzordnungen"""
|
|
S = _GibSession( SitzungsId )
|
|
if S['tische'] is None:
|
|
raise HTTPException( status_code=400,
|
|
detail='Erst das Tisch-Layout setzen (POST .../tische)' )
|
|
if S['personen'] is None:
|
|
raise HTTPException( status_code=400,
|
|
detail='Erst die Gaesteliste hochladen (POST .../personen)' )
|
|
|
|
TE = _TischeBauen( S )
|
|
P = S['personen']
|
|
|
|
# Vorab-Pruefungen mit klaren Fehlermeldungen (der Kern wirft sonst
|
|
# nur IndexError/AttributeError tief aus der Platzierung heraus)
|
|
GesamtLeute = P['nLeute'] + P['nVips']
|
|
if GesamtLeute > TE.NStuehle:
|
|
raise HTTPException( status_code=400,
|
|
detail='Zu wenig Stuehle: %d Personen, aber nur %d Plaetze'
|
|
% (GesamtLeute, TE.NStuehle) )
|
|
VIPsProTisch = {}
|
|
for Pid, Tid in P['VipHash'].items():
|
|
VIPsProTisch[Tid] = VIPsProTisch.get( Tid, 0 ) + 1
|
|
for Tid, AnzahlVIPs in VIPsProTisch.items():
|
|
T = TE.vonId( Tid )
|
|
if T is None:
|
|
raise HTTPException( status_code=400,
|
|
detail='VIP-Tisch %d existiert nicht im Layout' % Tid )
|
|
if AnzahlVIPs > T.Plaetze:
|
|
raise HTTPException( status_code=400,
|
|
detail='Tisch %d hat %d Plaetze, aber %d VIPs sind '
|
|
'dort fest gesetzt' % (Tid, T.Plaetze, AnzahlVIPs) )
|
|
|
|
try:
|
|
GAFarm = Farm( GAZyklus, Sitzplatzverteilung,
|
|
Tische=TE, Gruppen=P['GN'], Strafen=Strafen,
|
|
VIPListe=P['VipHash'], VIPs=P['VIPGruppe'] )
|
|
except IndexError as Fehler:
|
|
raise HTTPException( status_code=400, detail=str(Fehler) )
|
|
|
|
# Roadmap M2: nicht nur den Besten, sondern bis zu 'varianten'
|
|
# UNTERSCHIEDLICHE gute Loesungen aus dem Endpool — Farm.TopN liefert
|
|
# absteigend sortiert, die Signatur-Deduplizierung wirft Loesungen mit
|
|
# identischer Sitzordnung raus (Mutations-Verwandte). Liefert der Pool
|
|
# weniger unterschiedliche Sitzordnungen, kommen eben weniger zurueck
|
|
# (ein weiterer Klick auf Berechnen erzeugt einen frischen Lauf).
|
|
Ergebnisse, Gesehen = [], set()
|
|
for SV in GAFarm.TopN( len( GAFarm.Pool ) ):
|
|
Sig = _Signatur( SV )
|
|
if Sig in Gesehen:
|
|
continue
|
|
Gesehen.add( Sig )
|
|
Ergebnisse.append( SV )
|
|
if len( Ergebnisse ) == varianten:
|
|
break
|
|
|
|
S['ergebnisse'] = Ergebnisse
|
|
S['laeufe'] = S['laeufe'] + 1
|
|
return _ErgebnisJSON( S )
|
|
|
|
|
|
@app.get( '/sitzung/{SitzungsId}/ergebnis' )
|
|
def Ergebnis(SitzungsId: str):
|
|
"""alle Varianten des letzten Laufs als JSON (beste zuerst)"""
|
|
S = _GibSession( SitzungsId )
|
|
if not S['ergebnisse']:
|
|
raise HTTPException( status_code=404,
|
|
detail='Noch nichts berechnet (POST .../berechnen)' )
|
|
return _ErgebnisJSON( S )
|
|
|
|
|
|
@app.get( '/sitzung/{SitzungsId}/ergebnis.svg' )
|
|
def ErgebnisSVG(SitzungsId: str,
|
|
variante: int = Query( default=1, ge=1,
|
|
description='Nummer der Variante (1 = beste)' )):
|
|
"""eine berechnete Variante als SVG (alsSVG aus dem Kern)"""
|
|
S = _GibSession( SitzungsId )
|
|
if not S['ergebnisse']:
|
|
raise HTTPException( status_code=404,
|
|
detail='Noch nichts berechnet (POST .../berechnen)' )
|
|
if variante > len( S['ergebnisse'] ):
|
|
raise HTTPException( status_code=404,
|
|
detail='Variante %d gibt es nicht (letzter Lauf hat %d)'
|
|
% (variante, len( S['ergebnisse'] )) )
|
|
|
|
# alsSVG schreibt in eine Datei -> Tempdatei schreiben, lesen, loeschen
|
|
Handle, TempPfad = tempfile.mkstemp( suffix='.svg' )
|
|
os.close( Handle )
|
|
try:
|
|
S['ergebnisse'][variante - 1].alsSVG( TempPfad )
|
|
fsock = open( TempPfad, 'r' )
|
|
SVG = fsock.read()
|
|
fsock.close()
|
|
finally:
|
|
os.remove( TempPfad )
|
|
return Response( content=SVG, media_type='image/svg+xml' )
|
|
|
|
|
|
# statisches Test-Frontend unter / (muss nach den API-Routen gemountet sein)
|
|
app.mount( '/', StaticFiles(
|
|
directory=os.path.join( os.path.dirname( os.path.abspath( __file__ ) ),
|
|
'static' ),
|
|
html=True ), name='static' )
|