This commit is contained in:
hubobel 2025-10-05 10:53:22 +02:00
parent c9b4ee7af6
commit 844967f578
4 changed files with 501 additions and 7 deletions

303
6aus49APP/app.py Normal file
View file

@ -0,0 +1,303 @@
# app.py
import os
from datetime import date
from typing import List, Optional
from fastapi import FastAPI, Query, Request, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
from sqlalchemy.pool import NullPool
from dotenv import load_dotenv
# ------------------------------------------------------
# 0) Konfiguration laden
# ------------------------------------------------------
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
raise RuntimeError("DATABASE_URL nicht gesetzt (.env prüfen)")
PAGE_SIZE = int(os.getenv("PAGE_SIZE", "10")) # Standardlimit
# ------------------------------------------------------
# 1) App & DB initialisieren
# ------------------------------------------------------
engine: Engine = create_engine(DATABASE_URL, poolclass=NullPool, future=True)
app = FastAPI(title="Lotto-Auswertung (6aus49 / Eurojackpot)")
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
# ------------------------------------------------------
# 2) Pydantic-Schemas
# ------------------------------------------------------
class Draw(BaseModel):
datum: date
# gemeinsame Felder; bei Euro bleiben z6/sz/super6/spiel77 leer
z1: int; z2: int; z3: int; z4: int; z5: int
z6: Optional[int] = None
sz: Optional[int] = None
sz1: Optional[int] = None
sz2: Optional[int] = None
super6: Optional[str] = None
spiel77: Optional[str] = None
class DrawList(BaseModel):
total: int
items: List[Draw]
# ------------------------------------------------------
# 3) Datumsparser (Template Tabellename variabel)
# ------------------------------------------------------
DATE_EXPR_TMPL = (
"COALESCE("
" (CASE WHEN {tbl}.datum REGEXP '^[0-9]{{4}}-[0-9]{{2}}-[0-9]{{2}}$' THEN DATE({tbl}.datum) END),"
" STR_TO_DATE({tbl}.datum, '%d.%m.%Y'),"
" STR_TO_DATE({tbl}.datum, '%d.%m.%y'),"
" STR_TO_DATE(TRIM(SUBSTRING_INDEX({tbl}.datum, '/', -1)), '%d.%m.%Y'),"
" STR_TO_DATE(TRIM(SUBSTRING_INDEX({tbl}.datum, '/', -1)), '%d.%m.%y')"
")"
)
# ------------------------------------------------------
# 4) Helferfunktionen
# ------------------------------------------------------
def _normalize_game(s: str) -> str:
return "euro" if (s or "").lower() == "euro" else "6aus49"
def _to_date(s: Optional[str]):
if not s or s.strip() == "":
return None
try:
return date.fromisoformat(s.strip())
except Exception:
return None
def _rewrite_where_for_t_simple(where_parts):
if not where_parts:
return ""
return " WHERE " + " AND ".join(where_parts)
def _build_base_select(game: str) -> dict:
"""
Konfiguriert Spalten & Layout je nach Spiel.
Liefert:
- tbl: Tabellenname (mit Backticks)
- dx: Datumsausdruck
- base_select: Subquery-SELECT (liefert t.*)
- headers: Tabellenkopf (HTML)
- row_tpl: Zeilen-Template (HTML)
- link_prefix: Pfadpräfix für Detail-Link
"""
if game == "euro":
tbl = "`euro`"
dx = DATE_EXPR_TMPL.format(tbl=tbl)
base_select = """
SELECT
{dx} AS datum,
z1, z2, z3, z4, z5,
sz1, sz2
FROM {tbl}
""".format(dx=dx, tbl=tbl)
# 👇 Hier neue Spaltennamen
headers = "<tr><th>Datum</th><th>Zahlen (5)</th><th>Super 1</th><th>Super 2</th></tr>"
row_tpl = (
'<tr>'
'<td class="nowrap"><a href="{link}/{datum}" target="_blank">{datum}</a></td>'
'<td class="numbers">'
'<span class="badge">{z1}</span> <span class="badge">{z2}</span> <span class="badge">{z3}</span> '
'<span class="badge">{z4}</span> <span class="badge">{z5}</span>'
'</td>'
'<td class="nowrap"><strong>{sz1}</strong></td>'
'<td class="nowrap"><strong>{sz2}</strong></td>'
'</tr>'
)
link_prefix = "/api/draw/euro"
else:
tbl = "`6aus49`"
dx = DATE_EXPR_TMPL.format(tbl=tbl)
# super6/spiel77 als CHAR casten → Pydantic erwartet str
base_select = """
SELECT
{dx} AS datum,
z1, z2, z3, z4, z5, z6,
sz,
CAST(super6 AS CHAR) AS super6,
CAST(spiel77 AS CHAR) AS spiel77
FROM {tbl}
""".format(dx=dx, tbl=tbl)
headers = "<tr><th>Datum</th><th>Zahlen (6)</th><th>Superzahl</th><th>Super 6</th><th>Spiel 77</th></tr>"
row_tpl = (
'<tr>'
'<td class="nowrap"><a href="{link}/{datum}" target="_blank">{datum}</a></td>'
'<td class="numbers">'
'<span class="badge">{z1}</span> <span class="badge">{z2}</span> <span class="badge">{z3}</span> '
'<span class="badge">{z4}</span> <span class="badge">{z5}</span> <span class="badge">{z6}</span>'
'</td>'
'<td class="nowrap"><strong>{sz}</strong></td>'
'<td class="nowrap">{super6}</td>'
'<td class="nowrap">{spiel77}</td>'
'</tr>'
)
link_prefix = "/api/draw/6aus49"
return {
"tbl": tbl,
"dx": dx,
"base_select": base_select,
"headers": headers,
"row_tpl": row_tpl,
"link_prefix": link_prefix,
}
# ------------------------------------------------------
# 5) Startseite
# ------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "page_size": PAGE_SIZE})
# ------------------------------------------------------
# 6) Gemeinsamer HTML-Endpoint (HTMX)
# ------------------------------------------------------
@app.get("/ui/draws", response_class=HTMLResponse)
async def ui_draws(
request: Request,
game: str = Query("6aus49"),
date_from: Optional[str] = Query(None),
date_to: Optional[str] = Query(None),
limit: int = Query(10, ge=1, le=500),
offset: int = Query(0, ge=0),
order: str = Query("desc", pattern="^(asc|desc)$"),
):
game = _normalize_game(game)
cfg = _build_base_select(game)
d_from = _to_date(date_from)
d_to = _to_date(date_to)
where_parts = []
params = {}
if d_from:
where_parts.append("t.datum >= :date_from")
params["date_from"] = d_from
if d_to:
where_parts.append("t.datum <= :date_to")
params["date_to"] = d_to
where_sql = _rewrite_where_for_t_simple(where_parts)
if where_sql == "":
where_sql = " WHERE 1=1"
where_sql = where_sql + " AND t.datum IS NOT NULL"
order_sql = " ORDER BY t.datum DESC" if order.lower() == "desc" else " ORDER BY t.datum ASC"
count_sql = """
SELECT COUNT(*) AS cnt
FROM (
{base}
) AS t
{where_sql}
""".format(base=cfg["base_select"], where_sql=where_sql)
data_sql = """
SELECT *
FROM (
{base}
) AS t
{where_sql}
{order_sql}
LIMIT :limit OFFSET :offset
""".format(base=cfg["base_select"], where_sql=where_sql, order_sql=order_sql)
with engine.begin() as conn:
total = conn.execute(text(count_sql), params).scalar_one()
rows = conn.execute(text(data_sql), dict(params, limit=limit, offset=offset)).mappings()
body_rows = []
for r in rows:
d = dict(r)
d["link"] = cfg["link_prefix"]
body_rows.append(cfg["row_tpl"].format(**d))
html = """
<p class="muted">Treffer: {total}</p>
<table role="grid">
<thead>{headers}</thead>
<tbody>{rows}</tbody>
</table>
""".format(total=total, headers=cfg["headers"], rows="".join(body_rows))
return HTMLResponse(html)
# ------------------------------------------------------
# 7) Detail-Endpoint je Spiel (None-Felder ausblenden)
# ------------------------------------------------------
@app.get(
"/api/draw/{game}/{draw_date}",
response_model=Draw,
response_model_exclude_none=True
)
async def get_draw_game(game: str, draw_date: date):
game = _normalize_game(game)
if game == "euro":
dx = DATE_EXPR_TMPL.format(tbl="`euro`")
sql = text("""
SELECT t.datum,
t.z1, t.z2, t.z3, t.z4, t.z5,
NULL AS z6,
NULL AS sz,
t.sz1, t.sz2,
NULL AS super6,
NULL AS spiel77
FROM (
SELECT {dx} AS datum, z1, z2, z3, z4, z5, sz1, sz2
FROM `euro`
) AS t
WHERE t.datum = :d AND t.datum IS NOT NULL
""".format(dx=dx))
else:
dx = DATE_EXPR_TMPL.format(tbl="`6aus49`")
sql = text("""
SELECT t.datum,
t.z1, t.z2, t.z3, t.z4, t.z5, t.z6,
t.sz,
NULL AS sz1, NULL AS sz2,
t.super6, t.spiel77
FROM (
SELECT {dx} AS datum, z1, z2, z3, z4, z5, z6, sz,
CAST(super6 AS CHAR) AS super6,
CAST(spiel77 AS CHAR) AS spiel77
FROM `6aus49`
) AS t
WHERE t.datum = :d AND t.datum IS NOT NULL
""".format(dx=dx))
with engine.begin() as conn:
row = conn.execute(sql, {"d": draw_date}).mappings().first()
if not row:
raise HTTPException(status_code=404, detail="Ziehung nicht gefunden")
return Draw(**row)
# ------------------------------------------------------
# 8) Healthcheck
# ------------------------------------------------------
@app.get("/health")
async def health():
try:
with engine.begin() as conn:
conn.execute(text("SELECT 1"))
return {"status": "ok"}
except Exception as e:
return {"status": "error", "message": str(e)}
# ------------------------------------------------------
# 9) Lokaler Start
# ------------------------------------------------------
if __name__ == "__main__":
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)

View file

@ -0,0 +1,15 @@
fastapi==0.115.0
uvicorn[standard]==0.30.0
SQLAlchemy==2.0.34
PyMySQL==1.1.1
python-dotenv==1.0.1
jinja2==3.1.4
pydantic==2.9.2
fastapi==0.115.0
uvicorn[standard]==0.30.0
SQLAlchemy==2.0.34
psycopg2-binary==2.9.9
python-dotenv==1.0.1
jinja2==3.1.4
pydantic==2.9.2

View file

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Lotto-Ziehungen</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<link rel="icon" href="/static/favicon.ico">
<style>
body {
font-family: "Segoe UI", Arial, sans-serif;
background: #f6f6f6;
color: #222;
margin: 0;
padding: 2rem;
}
main {
max-width: 960px;
margin: 0 auto;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
padding: 2rem 2.5rem;
}
h1 {
margin-top: 0;
font-size: 1.9rem;
font-weight: 600;
text-align: center;
letter-spacing: 0.02em;
}
p.service-note {
text-align: center;
margin-top: 0.3rem;
margin-bottom: 1.8rem;
font-size: 0.95rem;
color: #666;
font-style: italic;
}
form {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.6rem;
margin-bottom: 1rem;
}
label { font-weight: 500; }
input, select, button {
padding: 0.35rem 0.6rem;
border: 1px solid #bbb;
border-radius: 4px;
background: #fff;
font-size: 0.95rem;
}
button {
background: #0a6;
color: #fff;
border: none;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button:hover { background: #098557; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
font-size: 0.95rem;
}
th, td {
border: 1px solid #ddd;
padding: 0.45rem 0.5rem;
text-align: center;
}
th {
background: #efefef;
font-weight: 600;
}
.numbers { text-align: left; }
.badge {
display: inline-block;
background: #0a6;
color: white;
padding: 0.25em 0.5em;
border-radius: 0.4em;
margin: 0 0.1em;
font-weight: 600;
}
.muted {
color: #555;
font-size: 0.9rem;
margin-top: 0.5rem;
text-align: right;
}
</style>
</head>
<body>
<main>
<h1>Lotto-Ziehungen</h1>
<p class="service-note">Ein Service der Hintergasse Angaben ohne Gewähr!</p>
<form hx-get="/ui/draws" hx-target="#results" hx-trigger="change, submit" hx-swap="innerHTML">
<label for="game">Spiel:</label>
<select name="game" id="game">
<option value="6aus49">6 aus 49</option>
<option value="euro">Eurojackpot</option>
</select>
<label for="date_from">von:</label>
<input type="date" name="date_from" />
<label for="date_to">bis:</label>
<input type="date" name="date_to" />
<label for="order">Sortierung:</label>
<select name="order">
<option value="desc" selected>neueste zuerst</option>
<option value="asc">älteste zuerst</option>
</select>
<label for="limit">Limit:</label>
<input type="number" name="limit" value="{{ page_size }}" min="1" max="500" />
<button type="submit">Anzeigen</button>
</form>
<div id="results" hx-get="/ui/draws" hx-trigger="load" hx-target="#results" hx-swap="innerHTML"></div>
</main>
</body>
</html>

View file

@ -71,6 +71,10 @@ def Euro():
bb = 'Eurozahl' + str(aa)
ZahlenEuro[Tag][bb] = int(c)
aa = aa + 1
jahr = datetime.now().year
tag = datetime.now().day
monat = datetime.now().month
ZahlenEuro[Tag]['Datum'] = f"{jahr}-{monat}-{tag}"
Tag = 'Dienstag'
for b in soup.find("p", string=lambda s: s and "dienstag" in s.lower()):
ZahlenEuro[Tag]['Datum'] = b
@ -91,10 +95,16 @@ def Euro():
bb = 'Eurozahl' + str(aa)
ZahlenEuro[Tag][bb] = int(i)
aa = aa + 1
jahr = datetime.now().year
tag = datetime.now().day
monat = datetime.now().month
ZahlenEuro[Tag]['Datum'] = f"{jahr}-{monat}-{tag}"
return ZahlenEuro
def Normalziehung(a):
wochentag = datetime.today().weekday()
jahr = datetime.now().year
tag = f"{datetime.now().day:02d}"
monat = datetime.now().month
url = "https://www.ard-text.de/mobil/" + str(a)
@ -125,7 +135,7 @@ def Normalziehung(a):
datum_woche = line.strip()
break
# Regex: Hauptzahlen finden (z.B. Zeile enthält "11 20 28 30 35 41")
match_haupt = re.search(r"\s(\d{1,2}(?:\s+\d{1,2}){5})\s", text)
match_haupt = re.search(r"\s(\d{1,2}(?:\s+\d{1,2}){6})\s", text)
if match_haupt:
lottozahlen = [int(n) for n in match_haupt.group(1).split()]
@ -163,7 +173,7 @@ def Normalziehung(a):
ef = str((ab + str(cd)))
Lottozahlen[ef] = i
cd = cd + 1
Lottozahlen['Datum'] = str(jahr) + ' / ' + str(datum_woche)
Lottozahlen['Datum'] = f"{jahr}-{monat}-{tag}"
Lottozahlen['Superzahl'] = superzahl
Lottozahlen['Spiel77'] = int(game77)
Lottozahlen['Super6'] = int(subber6)
@ -184,12 +194,18 @@ def SQLnorm(data):
"','" + str(data['Super6']) + "','" + str(data['Spiel77']) + "')"
sql_q = "SELECT * FROM 6aus49 WHERE datum like '%" + data['Datum'] + "%'"
resp = cursor.execute(sql_q)
if resp == 0:
cursor.execute(sql)
notify_telegram(str(data))
connection.commit()
cursor.close()
connection.close()
def SQLdienstag(data):
connection = pymysql.connect(db="hubobel",
user="hubobel",
@ -206,6 +222,7 @@ def SQLdienstag(data):
resp = cursor.execute(sql_q)
if resp == 0:
cursor.execute(sql)
notify_telegram(str(data))
#print(resp)
connection.commit()
cursor.close()
@ -225,6 +242,7 @@ def SQLfreitag(data):
resp = cursor.execute(sql_q)
if resp == 0:
cursor.execute(sql)
notify_telegram(str(data))
connection.commit()
cursor.close()
@ -264,30 +282,55 @@ def SQLmittwoch(data):
cursor.close()
connection.close()
def datum():
sql = """UPDATE `6aus49`
SET datum = DATE_FORMAT(
COALESCE(
STR_TO_DATE(TRIM(SUBSTRING_INDEX(datum, '/', -1)), '%d.%m.%Y'),
STR_TO_DATE(TRIM(SUBSTRING_INDEX(datum, '/', -1)), '%d.%m.%y')
),
'%Y-%m-%d'
)
WHERE
COALESCE(
STR_TO_DATE(TRIM(SUBSTRING_INDEX(datum, '/', -1)), '%d.%m.%Y'),
STR_TO_DATE(TRIM(SUBSTRING_INDEX(datum, '/', -1)), '%d.%m.%y')
) IS NOT NULL;"""
connection = pymysql.connect(db="hubobel",
user="hubobel",
passwd="polier2003",
host='10.0.1.123', charset='utf8')
cursor = connection.cursor()
cursor.execute(sql)
connection.commit()
cursor.close()
connection.close()
passw = conf()
telegram_chat_id = passw['Telegram']['Chat_ID']
telegram_token = passw['Telegram']['TOKEN']
wochentag = datetime.today().weekday()
wochentag = 1
wochentag = 5
if wochentag == 2:
Zahl = Normalziehung(582)
SQLnorm(Zahl)
SQLmittwoch(Zahl)
notify_telegram(str(Zahl))
#notify_telegram(str(Zahl))
elif wochentag == 5:
Zahl = Normalziehung(581)
SQLnorm(Zahl)
SQLsamstag(Zahl)
notify_telegram(str(Zahl))
#notify_telegram(str(Zahl))
elif wochentag == 1:
Zahl = Euro()
SQLdienstag(Zahl['Dienstag'])
notify_telegram(str(Zahl['Dienstag']))
#notify_telegram(str(Zahl['Dienstag']))
elif wochentag == 4:
Zahl = Euro()
SQLfreitag(Zahl['Freitag'])
notify_telegram(str(Zahl['Freitag']))
#notify_telegram(str(Zahl['Freitag']))
else:
quit()