Erstumsetzung: OAMC Turnierauswertung (AP 0–7, 9)

* Datenmodell + Alembic-Initialmigration (Plan §4), Seed Saison 2026
* Wertungs-Engine (Plan §3.3), regelbasiert je Kategorie/Saison
  - Abnahmetest gegen die Ergebnisliste 2026-05-31
* Meisterschaftsberechnung mit Streichresultaten + Tie-Break
  (PLATZHALTER-Punktetabelle, O-1 offen)
* Ingest-API (idempotent, dry_run) inkl. Excel-Leser und Fahrer-Matching/Merge
* Zeitmessungs-Endpoint (Batch, idempotent, unzugeordnet/unplausibel) + SSE-Anzeige
  - Mess-Pi-Client bewusst NICHT enthalten (docs/zeitmessung-endpoint.md)
* Frontend: öffentliche Seiten + internes Backend (Jinja2/HTMX, kein Build)
* Auslieferung: Docker (multi-arch) + docker-compose sowie Debian-Paket/APT-Repo
* 57 Tests grün gegen echte PostgreSQL-Testdatenbank

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
linus
2026-09-05 18:49:00 +02:00
co-authored by Claude Sonnet 5
commit 03f6fdb0d9
115 changed files with 8041 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
"""Stammdaten-Seed (AP 1).
``seed_saison_2026`` legt Saison 2026, Klassen J 1-J 5 + S/A, Vereine und die
HTH-Meisterschaften an. Idempotent: mehrfaches Ausfuehren aendert nichts.
Offene Fragen, die den Seed betreffen:
* O-1 Punktetabelle Meisterschaft -> Platzhalter in ``meisterschaft.regelwerk``
* O-2 Zuordnung S 1-S 9 / A 1-A 4 -> Fahrzeugtyp -> ``fahrzeugtyp`` bleibt NULL
"""
from oamc.seeds.saison_2026 import seed_saison_2026
__all__ = ["seed_saison_2026"]
+146
View File
@@ -0,0 +1,146 @@
"""Seed fuer Saison 2026 — abgeleitet aus plan.md §3.2 und §3.4."""
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.orm import Session
from oamc.meisterschaft.berechnung import PLATZHALTER_PUNKTETABELLE
from oamc.models import Klasse, Meisterschaft, Saison, Verein
from oamc.models.enums import Kategorie
# "Turniere in der Saison" -> "davon gewertet" (Plan §3.4)
STREICHRESULTATE_2026 = {
"12": 8, "11": 8, "10": 8, "9": 7, "8": 6, "7": 5, "6": 4, "5": 3, "4": 2,
}
# Jugendklassen: Einteilung strikt nach Jahrgang (Plan §3.2)
JUGENDKLASSEN = [
("J 1", [2019, 2018, 2017], 110, 5.5, "Kindermotorrad", 7),
("J 2", [2016, 2015], 110, 5.5, "Kindermotorrad", None),
("J 3", [2014, 2013], 125, 11.0, "Mofa / Motorrad / Roller", None),
("J 4", [2012, 2011], 125, 11.0, "Mofa / Motorrad / Roller", None),
("J 5", [2010, 2009, 2008], 125, 11.0, "Mofa / Motorrad / Roller (bis 18. Geburtstag)", None),
]
# Erwachsenenklassen: konkrete Fahrzeugzuordnung ist offene Frage O-2.
STAMMKLASSEN = [f"S {i}" for i in range(1, 10)]
ANFAENGERKLASSEN = [f"A {i}" for i in range(1, 5)]
VEREINE = [
("OAMC Reinheim e.V. im ADAC", "OAMC Reinheim", "ADAC Hessen-Thueringen"),
("MSC Hainstadt", "Hainstadt", "ADAC Hessen-Thueringen"),
("MSC Schotten", "Schotten", "ADAC Hessen-Thueringen"),
("PMS Kassel", "Kassel", "ADAC Hessen-Thueringen"),
]
def _get_or_create(db: Session, model, defaults=None, **kwargs):
obj = db.scalars(select(model).filter_by(**kwargs)).first()
if obj is not None:
return obj, False
obj = model(**kwargs, **(defaults or {}))
db.add(obj)
db.flush()
return obj, True
def seed_saison_2026(db: Session) -> dict[str, int]:
"""Idempotenter Seed. Rueckgabe: Zaehler der neu angelegten Objekte."""
zaehler = {"saison": 0, "verein": 0, "klasse": 0, "meisterschaft": 0}
saison, neu = _get_or_create(
db,
Saison,
jahr=2026,
defaults={
"turnierordnung_version": "MT-D2025 (zu verifizieren, Plan §10)",
"streichresultate_tabelle": STREICHRESULTATE_2026,
"nenngeld_cent": 1000,
"aktiv": True,
},
)
zaehler["saison"] += int(neu)
for name, kurz, gau in VEREINE:
_, neu = _get_or_create(
db, Verein, name=name, defaults={"kurzname": kurz, "adac_gau": gau, "aktiv": True}
)
zaehler["verein"] += int(neu)
sortierung = 0
for code, jahrgaenge, ccm, kw, typ, mindestalter in JUGENDKLASSEN:
sortierung += 1
_, neu = _get_or_create(
db,
Klasse,
saison_jahr=2026,
code=code,
defaults={
"kategorie": Kategorie.JUGEND,
"bezeichnung": f"Jugend {code} (Jahrgang {'/'.join(map(str, jahrgaenge))})",
"jahrgaenge": jahrgaenge,
"ccm_max": ccm,
"kw_max": kw,
"fahrzeugtyp": typ,
"mindestalter_am_veranstaltungstag": mindestalter,
"sortierung": sortierung,
},
)
zaehler["klasse"] += int(neu)
for code in STAMMKLASSEN:
sortierung += 1
_, neu = _get_or_create(
db, Klasse, saison_jahr=2026, code=code,
defaults={
"kategorie": Kategorie.STAMM,
"bezeichnung": f"Stammfahrer {code}",
"fahrzeugtyp": None,
"sortierung": sortierung,
"notiz": "O-2 offen: Fahrzeugtyp/Hubraum/kW noch zuzuordnen",
},
)
zaehler["klasse"] += int(neu)
for code in ANFAENGERKLASSEN:
sortierung += 1
_, neu = _get_or_create(
db, Klasse, saison_jahr=2026, code=code,
defaults={
"kategorie": Kategorie.ANFAENGER,
"bezeichnung": f"Anfaenger {code}",
"fahrzeugtyp": None,
"sortierung": sortierung,
"notiz": "O-2 offen: Fahrzeugtyp/Hubraum/kW noch zuzuordnen",
},
)
zaehler["klasse"] += int(neu)
platzhalter_regelwerk = {
"punkte": {str(k): v for k, v in PLATZHALTER_PUNKTETABELLE.items()},
"_hinweis": "O-1 offen: PLATZHALTER-Punktetabelle, nicht offiziell!",
}
_, neu = _get_or_create(
db, Meisterschaft, saison_jahr=2026, bezeichnung="HTH Erwachsene 2026",
defaults={"kategorie": Kategorie.STAMM, "regelwerk": platzhalter_regelwerk},
)
zaehler["meisterschaft"] += int(neu)
jugendklassen = db.scalars(
select(Klasse).where(Klasse.saison_jahr == 2026, Klasse.kategorie == Kategorie.JUGEND)
).all()
for k in jugendklassen:
_, neu = _get_or_create(
db, Meisterschaft, saison_jahr=2026, bezeichnung=f"HTH Jugend {k.code} 2026",
defaults={
"kategorie": Kategorie.JUGEND,
"klasse_id": k.id,
"regelwerk": platzhalter_regelwerk,
},
)
zaehler["meisterschaft"] += int(neu)
return zaehler