From 844967f57864ba4c469c6d4f8dbcc247773be6db Mon Sep 17 00:00:00 2001 From: hubobel Date: Sun, 5 Oct 2025 10:53:22 +0200 Subject: [PATCH] WebApp --- 6aus49APP/app.py | 303 +++++++++++++++++++++++++++++++++ 6aus49APP/requirements.txt | 15 ++ 6aus49APP/templates/index.html | 133 +++++++++++++++ lotto2py.py | 57 ++++++- 4 files changed, 501 insertions(+), 7 deletions(-) create mode 100644 6aus49APP/app.py create mode 100644 6aus49APP/requirements.txt create mode 100644 6aus49APP/templates/index.html diff --git a/6aus49APP/app.py b/6aus49APP/app.py new file mode 100644 index 0000000..c13a0b0 --- /dev/null +++ b/6aus49APP/app.py @@ -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 = "DatumZahlen (5)Super 1Super 2" + row_tpl = ( + '' + '{datum}' + '' + '{z1} {z2} {z3} ' + '{z4} {z5}' + '' + '{sz1}' + '{sz2}' + '' + ) + 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 = "DatumZahlen (6)SuperzahlSuper 6Spiel 77" + row_tpl = ( + '' + '{datum}' + '' + '{z1} {z2} {z3} ' + '{z4} {z5} {z6}' + '' + '{sz}' + '{super6}' + '{spiel77}' + '' + ) + 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 = """ +

Treffer: {total}

+ + {headers} + {rows} +
+ """.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) diff --git a/6aus49APP/requirements.txt b/6aus49APP/requirements.txt new file mode 100644 index 0000000..e6df49f --- /dev/null +++ b/6aus49APP/requirements.txt @@ -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 + diff --git a/6aus49APP/templates/index.html b/6aus49APP/templates/index.html new file mode 100644 index 0000000..9b8649f --- /dev/null +++ b/6aus49APP/templates/index.html @@ -0,0 +1,133 @@ + + + + + Lotto-Ziehungen + + + + + + +
+

Lotto-Ziehungen

+

Ein Service der Hintergasse – Angaben ohne Gewähr!

+ +
+ + + + + + + + + + + + + + + + +
+ +
+
+ + diff --git a/lotto2py.py b/lotto2py.py index 61bbca4..731f0db 100644 --- a/lotto2py.py +++ b/lotto2py.py @@ -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()