05/23 Phase 7

This commit is contained in:
2026-05-23 11:58:51 -04:00
parent c1405b5fb8
commit 8fc1878670
8 changed files with 638 additions and 15 deletions
+2 -1
View File
@@ -7,7 +7,8 @@
"Bash(python -m pytest tests/test_analyzer.py -v)", "Bash(python -m pytest tests/test_analyzer.py -v)",
"Bash(python -m pytest tests/test_predictor.py -v)", "Bash(python -m pytest tests/test_predictor.py -v)",
"Bash(python -m pytest)", "Bash(python -m pytest)",
"Bash(python -m pytest tests/test_settings.py -v)" "Bash(python -m pytest tests/test_settings.py -v)",
"Bash(python -m pytest tests/ -v --tb=short)"
] ]
} }
} }
+15 -10
View File
@@ -307,16 +307,21 @@ All actions are logged to console and optionally to a log file:
--- ---
### 🔲 Phase 7 — Export + Polish ### Phase 7 — Export + Polish
- [ ] Add export to Excel (`openpyxl`) for: - [x] Write `core/exporter.py`
- [ ] Draw history - [x] `export_draws_excel(filepath, game_id, date_from, date_to)` — openpyxl, styled header
- [ ] Predictions - [x] `export_draws_csv(filepath, game_id, date_from, date_to)` — stdlib csv
- [ ] Frequency analysis - [x] `export_predictions_excel(filepath, game_id)` — openpyxl
- [ ] Add export to CSV - [x] `export_predictions_csv(filepath, game_id)` — stdlib csv
- [ ] Add `assets/icon.png` - [x] `export_frequency_excel(filepath, game_id, last_n)` — number + freq + gap
- [ ] App title bar + icon - [x] `create_icon_png(path, size)` — valid PNG, stdlib only (struct + zlib)
- [ ] Window min-size + resizable layout - [x] Add Export Excel + Export CSV buttons to History screen (filter-aware)
- [ ] Error handling — network down, API timeout, bad CSV - [x] Add Export Excel + Export CSV buttons to Predictor screen (DB predictions)
- [x] Add Export Frequency button to Analysis screen (respects frequency window)
- [x] Auto-create `assets/icon.png` on startup if missing
- [x] Window min-size set (900×600) + resizable layout (all screens)
- [x] Error handling — try/except + messagebox.showerror on all export paths
- [x] 27 tests for exporter (165/165 total passing)
--- ---
+202
View File
@@ -0,0 +1,202 @@
"""
core/exporter.py
----------------
Export draw history, predictions, and frequency analysis
to Excel (.xlsx via openpyxl) and CSV.
All functions accept a filepath and write to it.
Callers (UI) handle file-dialog; tests pass tmp paths directly.
"""
import csv
import os
import struct
import zlib
from datetime import datetime
import openpyxl
from openpyxl.styles import Alignment, Font, PatternFill
from db.models import get_draws_with_game, get_game_by_id, get_predictions
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
EXPORTS_DIR = os.path.join(BASE_DIR, "exports")
def ensure_exports_dir():
os.makedirs(EXPORTS_DIR, exist_ok=True)
def default_path(stem: str, ext: str) -> str:
ensure_exports_dir()
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
return os.path.join(EXPORTS_DIR, f"{stem}_{ts}.{ext}")
# ── Shared helpers ────────────────────────────────────────────────────────────
def _header_row(ws, labels: list[str], fill_hex: str):
bold_white = Font(bold=True, color="FFFFFF")
fill = PatternFill(fill_type="solid", fgColor=fill_hex)
center = Alignment(horizontal="center")
for col, label in enumerate(labels, start=1):
cell = ws.cell(row=1, column=col, value=label)
cell.font = bold_white
cell.fill = fill
cell.alignment = center
def _autofit(ws):
for col in ws.columns:
width = max((len(str(cell.value or "")) for cell in col), default=0)
ws.column_dimensions[col[0].column_letter].width = min(width + 4, 60)
# ── Draw History ──────────────────────────────────────────────────────────────
_DRAW_HEADERS = ["Game", "Date", "Numbers", "Bonus", "Multiplier", "Source"]
def export_draws_excel(filepath: str, game_id=None, date_from=None, date_to=None):
"""Write draw history to filepath (.xlsx). Returns filepath."""
rows = get_draws_with_game(game_id=game_id, date_from=date_from,
date_to=date_to, order="DESC")
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Draw History"
_header_row(ws, _DRAW_HEADERS, "2C3E50")
for i, row in enumerate(rows, start=2):
ws.cell(i, 1, row["game_name"])
ws.cell(i, 2, row["draw_date"])
ws.cell(i, 3, row["numbers"])
ws.cell(i, 4, row["bonus"] or "")
ws.cell(i, 5, row["multiplier"] or "")
ws.cell(i, 6, row["source"] or "")
_autofit(ws)
wb.save(filepath)
return filepath
def export_draws_csv(filepath: str, game_id=None, date_from=None, date_to=None):
"""Write draw history to filepath (.csv). Returns filepath."""
rows = get_draws_with_game(game_id=game_id, date_from=date_from,
date_to=date_to, order="DESC")
with open(filepath, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(_DRAW_HEADERS)
for row in rows:
w.writerow([row["game_name"], row["draw_date"], row["numbers"],
row["bonus"] or "", row["multiplier"] or "",
row["source"] or ""])
return filepath
# ── Predictions ───────────────────────────────────────────────────────────────
_PRED_HEADERS = ["ID", "Game", "Strategy", "Numbers", "Bonus", "Created"]
def export_predictions_excel(filepath: str, game_id=None):
"""Write saved predictions to filepath (.xlsx). Returns filepath."""
preds = get_predictions(game_id=game_id, limit=100_000)
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Predictions"
_header_row(ws, _PRED_HEADERS, "1A5276")
for i, p in enumerate(preds, start=2):
ws.cell(i, 1, p["id"])
ws.cell(i, 2, p["game_name"])
ws.cell(i, 3, p["strategy"])
ws.cell(i, 4, p["numbers"])
ws.cell(i, 5, p["bonus"] or "")
ws.cell(i, 6, p["created_at"][:16])
_autofit(ws)
wb.save(filepath)
return filepath
def export_predictions_csv(filepath: str, game_id=None):
"""Write saved predictions to filepath (.csv). Returns filepath."""
preds = get_predictions(game_id=game_id, limit=100_000)
with open(filepath, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(_PRED_HEADERS)
for p in preds:
w.writerow([p["id"], p["game_name"], p["strategy"],
p["numbers"], p["bonus"] or "", p["created_at"][:16]])
return filepath
# ── Frequency Analysis ────────────────────────────────────────────────────────
_FREQ_HEADERS = ["Number", "Frequency", "Gap (draws since last seen)"]
def export_frequency_excel(filepath: str, game_id: int, last_n=None):
"""Write frequency + gap analysis to filepath (.xlsx). Returns filepath."""
from core.analyzer import frequency_analysis, gap_analysis
freq = frequency_analysis(game_id, last_n=last_n)
gaps = gap_analysis(game_id)
game = get_game_by_id(game_id)
all_nums = range(1, game["main_max"] + 1)
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Frequency Analysis"
_header_row(ws, _FREQ_HEADERS, "1E8449")
for i, num in enumerate(all_nums, start=2):
ws.cell(i, 1, num)
ws.cell(i, 2, freq.get(num, 0))
ws.cell(i, 3, gaps.get(num, ""))
_autofit(ws)
wb.save(filepath)
return filepath
# ── App icon (stdlib only, no Pillow required) ────────────────────────────────
def create_icon_png(path: str, size: int = 48):
"""
Write a simple lottery-ball icon as a valid PNG to path.
Uses only stdlib (struct + zlib) — no Pillow needed.
"""
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
cx = cy = size // 2
r_outer = cx - 2
r_inner = cx - 10
rows = []
for y in range(size):
row = b"\x00" # filter byte = None
for x in range(size):
d2 = (x - cx) ** 2 + (y - cy) ** 2
if d2 <= r_outer * r_outer:
if d2 <= r_inner * r_inner:
row += bytes([255, 255, 255]) # white centre
else:
row += bytes([44, 110, 203]) # blue ring
else:
row += bytes([235, 235, 235]) # light-grey bg
rows.append(row)
def _chunk(tag: bytes, data: bytes) -> bytes:
payload = tag + data
return (struct.pack(">I", len(data)) + payload
+ struct.pack(">I", zlib.crc32(payload) & 0xFFFFFFFF))
png = (
b"\x89PNG\r\n\x1a\n"
+ _chunk(b"IHDR", struct.pack(">II5B", size, size, 8, 2, 0, 0, 0))
+ _chunk(b"IDAT", zlib.compress(b"".join(rows), 9))
+ _chunk(b"IEND", b"")
)
with open(path, "wb") as fh:
fh.write(png)
+5
View File
@@ -7,6 +7,7 @@ Wires auto-fetch on launch (background thread) and 24hr APScheduler job.
""" """
import logging import logging
import os
import threading import threading
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
@@ -15,6 +16,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
from db.database import init_db from db.database import init_db
from core.fetcher import fetch_all from core.fetcher import fetch_all
from core.exporter import create_icon_png
from ui.statusbar import StatusBar from ui.statusbar import StatusBar
from ui.history import HistoryScreen from ui.history import HistoryScreen
from ui.analysis import AnalysisScreen from ui.analysis import AnalysisScreen
@@ -170,6 +172,9 @@ class LottoSightApp(tk.Tk):
def main(): def main():
init_db() init_db()
if not os.path.exists("assets/icon.png"):
os.makedirs("assets", exist_ok=True)
create_icon_png("assets/icon.png")
app = LottoSightApp() app = LottoSightApp()
app.protocol("WM_DELETE_WINDOW", app.on_close) app.protocol("WM_DELETE_WINDOW", app.on_close)
app.mainloop() app.mainloop()
+287
View File
@@ -0,0 +1,287 @@
"""
tests/test_exporter.py
-----------------------
Tests for core/exporter.py — draw history, predictions, frequency analysis exports.
All tests write to tmp paths; no file-dialog interaction required.
"""
import csv
import os
import openpyxl
import pytest
from db.models import get_game_by_name, insert_draw, insert_prediction
from core.exporter import (
export_draws_excel,
export_draws_csv,
export_predictions_excel,
export_predictions_csv,
export_frequency_excel,
create_icon_png,
ensure_exports_dir,
default_path,
EXPORTS_DIR,
)
# ── Fixtures ──────────────────────────────────────────────────────────────────
@pytest.fixture()
def pb(tmp_db):
return get_game_by_name("Powerball")
@pytest.fixture()
def mm(tmp_db):
return get_game_by_name("Mega Millions")
@pytest.fixture()
def populated_db(pb, mm):
insert_draw(pb["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7, multiplier="2x")
insert_draw(pb["id"], "2024-01-03", [5, 10, 20, 30, 40], bonus=15, multiplier="3x")
insert_draw(mm["id"], "2024-01-02", [2, 14, 37, 62, 70], bonus=5)
insert_prediction(pb["id"], "Hot Numbers", [1, 13, 36, 61, 69], bonus=7)
insert_prediction(mm["id"], "Due Numbers", [2, 14, 37, 62, 70], bonus=5)
return pb, mm
# ── Draw Excel ────────────────────────────────────────────────────────────────
def test_draws_excel_creates_file(tmp_path, populated_db):
fp = str(tmp_path / "draws.xlsx")
result = export_draws_excel(fp)
assert result == fp
assert os.path.exists(fp)
def test_draws_excel_header(tmp_path, populated_db):
fp = str(tmp_path / "draws.xlsx")
export_draws_excel(fp)
wb = openpyxl.load_workbook(fp)
ws = wb.active
headers = [ws.cell(1, c).value for c in range(1, 7)]
assert headers == ["Game", "Date", "Numbers", "Bonus", "Multiplier", "Source"]
def test_draws_excel_row_count(tmp_path, populated_db):
pb, mm = populated_db
fp = str(tmp_path / "draws.xlsx")
export_draws_excel(fp)
wb = openpyxl.load_workbook(fp)
ws = wb.active
# 3 draws inserted (header is row 1)
assert ws.max_row == 4
def test_draws_excel_game_filter(tmp_path, populated_db):
pb, mm = populated_db
fp = str(tmp_path / "pb_only.xlsx")
export_draws_excel(fp, game_id=pb["id"])
wb = openpyxl.load_workbook(fp)
ws = wb.active
games_in_file = {ws.cell(r, 1).value for r in range(2, ws.max_row + 1)}
assert games_in_file == {"Powerball"}
def test_draws_excel_date_filter(tmp_path, populated_db):
fp = str(tmp_path / "filtered.xlsx")
export_draws_excel(fp, date_from="2024-01-03", date_to="2024-01-03")
wb = openpyxl.load_workbook(fp)
ws = wb.active
# Only the 2024-01-03 draw
assert ws.max_row == 2
def test_draws_excel_empty_db(tmp_path, tmp_db):
fp = str(tmp_path / "empty.xlsx")
export_draws_excel(fp)
wb = openpyxl.load_workbook(fp)
ws = wb.active
assert ws.max_row == 1 # header only
# ── Draw CSV ──────────────────────────────────────────────────────────────────
def test_draws_csv_creates_file(tmp_path, populated_db):
fp = str(tmp_path / "draws.csv")
result = export_draws_csv(fp)
assert result == fp
assert os.path.exists(fp)
def test_draws_csv_header(tmp_path, populated_db):
fp = str(tmp_path / "draws.csv")
export_draws_csv(fp)
with open(fp, newline="", encoding="utf-8") as f:
reader = csv.reader(f)
header = next(reader)
assert header == ["Game", "Date", "Numbers", "Bonus", "Multiplier", "Source"]
def test_draws_csv_row_count(tmp_path, populated_db):
fp = str(tmp_path / "draws.csv")
export_draws_csv(fp)
with open(fp, newline="", encoding="utf-8") as f:
rows = list(csv.reader(f))
assert len(rows) == 4 # 1 header + 3 draws
def test_draws_csv_game_filter(tmp_path, populated_db):
pb, _ = populated_db
fp = str(tmp_path / "pb.csv")
export_draws_csv(fp, game_id=pb["id"])
with open(fp, newline="", encoding="utf-8") as f:
rows = list(csv.reader(f))
data_rows = rows[1:]
assert all(r[0] == "Powerball" for r in data_rows)
# ── Predictions Excel ─────────────────────────────────────────────────────────
def test_predictions_excel_creates_file(tmp_path, populated_db):
fp = str(tmp_path / "preds.xlsx")
result = export_predictions_excel(fp)
assert result == fp
assert os.path.exists(fp)
def test_predictions_excel_header(tmp_path, populated_db):
fp = str(tmp_path / "preds.xlsx")
export_predictions_excel(fp)
wb = openpyxl.load_workbook(fp)
ws = wb.active
headers = [ws.cell(1, c).value for c in range(1, 7)]
assert headers == ["ID", "Game", "Strategy", "Numbers", "Bonus", "Created"]
def test_predictions_excel_row_count(tmp_path, populated_db):
fp = str(tmp_path / "preds.xlsx")
export_predictions_excel(fp)
wb = openpyxl.load_workbook(fp)
ws = wb.active
assert ws.max_row == 3 # 1 header + 2 predictions
def test_predictions_excel_game_filter(tmp_path, populated_db):
pb, _ = populated_db
fp = str(tmp_path / "pb_preds.xlsx")
export_predictions_excel(fp, game_id=pb["id"])
wb = openpyxl.load_workbook(fp)
ws = wb.active
games = {ws.cell(r, 2).value for r in range(2, ws.max_row + 1)}
assert games == {"Powerball"}
# ── Predictions CSV ───────────────────────────────────────────────────────────
def test_predictions_csv_creates_file(tmp_path, populated_db):
fp = str(tmp_path / "preds.csv")
result = export_predictions_csv(fp)
assert result == fp
assert os.path.exists(fp)
def test_predictions_csv_header(tmp_path, populated_db):
fp = str(tmp_path / "preds.csv")
export_predictions_csv(fp)
with open(fp, newline="", encoding="utf-8") as f:
header = next(csv.reader(f))
assert header == ["ID", "Game", "Strategy", "Numbers", "Bonus", "Created"]
def test_predictions_csv_row_count(tmp_path, populated_db):
fp = str(tmp_path / "preds.csv")
export_predictions_csv(fp)
with open(fp, newline="", encoding="utf-8") as f:
rows = list(csv.reader(f))
assert len(rows) == 3 # 1 header + 2 predictions
# ── Frequency Excel ───────────────────────────────────────────────────────────
def test_frequency_excel_creates_file(tmp_path, populated_db):
pb, _ = populated_db
fp = str(tmp_path / "freq.xlsx")
result = export_frequency_excel(fp, pb["id"])
assert result == fp
assert os.path.exists(fp)
def test_frequency_excel_header(tmp_path, populated_db):
pb, _ = populated_db
fp = str(tmp_path / "freq.xlsx")
export_frequency_excel(fp, pb["id"])
wb = openpyxl.load_workbook(fp)
ws = wb.active
headers = [ws.cell(1, c).value for c in range(1, 4)]
assert headers == ["Number", "Frequency", "Gap (draws since last seen)"]
def test_frequency_excel_covers_full_pool(tmp_path, populated_db):
pb, _ = populated_db
fp = str(tmp_path / "freq.xlsx")
export_frequency_excel(fp, pb["id"])
wb = openpyxl.load_workbook(fp)
ws = wb.active
# Powerball main_max=69: 69 data rows + 1 header = 70
assert ws.max_row == 70
def test_frequency_excel_number_column(tmp_path, populated_db):
pb, _ = populated_db
fp = str(tmp_path / "freq.xlsx")
export_frequency_excel(fp, pb["id"])
wb = openpyxl.load_workbook(fp)
ws = wb.active
numbers = [ws.cell(r, 1).value for r in range(2, ws.max_row + 1)]
assert numbers == list(range(1, 70))
# ── Icon PNG ──────────────────────────────────────────────────────────────────
def test_create_icon_png_creates_file(tmp_path):
path = str(tmp_path / "icon.png")
create_icon_png(path)
assert os.path.exists(path)
def test_create_icon_png_is_valid_png(tmp_path):
path = str(tmp_path / "icon.png")
create_icon_png(path)
with open(path, "rb") as f:
sig = f.read(8)
assert sig == b"\x89PNG\r\n\x1a\n"
def test_create_icon_png_default_size(tmp_path):
path = str(tmp_path / "icon.png")
create_icon_png(path)
size = os.path.getsize(path)
assert size > 100 # a valid non-empty PNG
def test_create_icon_png_custom_size(tmp_path):
path = str(tmp_path / "icon32.png")
create_icon_png(path, size=32)
assert os.path.exists(path)
# ── Helpers ───────────────────────────────────────────────────────────────────
def test_ensure_exports_dir_creates_directory(tmp_path, monkeypatch):
target = str(tmp_path / "new_exports")
monkeypatch.setattr("core.exporter.EXPORTS_DIR", target)
import core.exporter as exp
exp.EXPORTS_DIR = target
exp.ensure_exports_dir()
assert os.path.isdir(target)
def test_default_path_returns_string_with_stem(tmp_path, monkeypatch):
import core.exporter as exp
monkeypatch.setattr(exp, "EXPORTS_DIR", str(tmp_path))
path = exp.default_path("draws", "xlsx")
assert "draws" in os.path.basename(path)
assert path.endswith(".xlsx")
+23 -1
View File
@@ -7,8 +7,9 @@ Analysis screen — three Matplotlib charts embedded in a ttk.Notebook.
• Gap — draws since each number last appeared • Gap — draws since each number last appeared
""" """
import os
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk, filedialog, messagebox
import numpy as np import numpy as np
import matplotlib import matplotlib
@@ -18,6 +19,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolb
from db.models import get_all_games, get_game_by_id, get_game_by_name from db.models import get_all_games, get_game_by_id, get_game_by_name
from core.analyzer import frequency_analysis, gap_analysis, positional_frequency from core.analyzer import frequency_analysis, gap_analysis, positional_frequency
from core.exporter import export_frequency_excel, ensure_exports_dir
_LAST_N_OPTIONS = { _LAST_N_OPTIONS = {
"All draws": None, "All draws": None,
@@ -60,6 +62,7 @@ class AnalysisScreen(ttk.Frame):
last_n_cb.bind("<<ComboboxSelected>>", lambda _: self._redraw_frequency()) last_n_cb.bind("<<ComboboxSelected>>", lambda _: self._redraw_frequency())
ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right") ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right")
ttk.Button(bar, text="Export Frequency", command=self._export_frequency).pack(side="right", padx=(0, 4))
# Notebook with three chart tabs # Notebook with three chart tabs
nb = ttk.Notebook(self) nb = ttk.Notebook(self)
@@ -141,6 +144,25 @@ class AnalysisScreen(ttk.Frame):
_draw_gap(ax, self._game_id) _draw_gap(ax, self._game_id)
self._gap_canvas.draw() self._gap_canvas.draw()
def _export_frequency(self):
if self._game_id is None:
messagebox.showinfo("Export", "Select a game first.")
return
ensure_exports_dir()
fp = filedialog.asksaveasfilename(
title="Export frequency analysis",
defaultextension=".xlsx",
filetypes=[("Excel workbook", "*.xlsx")],
initialfile=f"frequency_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.xlsx",
)
if not fp:
return
try:
last_n = _LAST_N_OPTIONS.get(self._last_n_var.get())
export_frequency_excel(fp, self._game_id, last_n=last_n)
except Exception as e:
messagebox.showerror("Export failed", str(e))
# ── Pure chart-drawing functions (no Tkinter, just axes) ───────────────────── # ── Pure chart-drawing functions (no Tkinter, just axes) ─────────────────────
+59 -2
View File
@@ -5,10 +5,14 @@ Draw History browser screen.
Shows a sortable, filterable Treeview of all draw records. Shows a sortable, filterable Treeview of all draw records.
""" """
import os
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk, filedialog, messagebox
from db.models import get_all_games, get_game_by_name, get_draws_with_game from db.models import get_all_games, get_game_by_name, get_draws_with_game
from core.exporter import (
export_draws_excel, export_draws_csv, default_path, ensure_exports_dir,
)
_COLUMNS = ("game", "date", "numbers", "bonus", "multiplier", "source") _COLUMNS = ("game", "date", "numbers", "bonus", "multiplier", "source")
_LABELS = { _LABELS = {
@@ -77,7 +81,9 @@ class HistoryScreen(ttk.Frame):
ttk.Button(bar, text="Search", command=self._apply_filter).pack(side="left", padx=(0, 4)) ttk.Button(bar, text="Search", command=self._apply_filter).pack(side="left", padx=(0, 4))
ttk.Button(bar, text="Clear", command=self._clear_filter).pack(side="left") ttk.Button(bar, text="Clear", command=self._clear_filter).pack(side="left")
ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right") ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right")
ttk.Button(bar, text="Export CSV", command=self._export_csv).pack(side="right", padx=(0, 4))
ttk.Button(bar, text="Export Excel",command=self._export_excel).pack(side="right", padx=(0, 4))
# ── Treeview + scrollbars ───────────────────────────────────────────── # ── Treeview + scrollbars ─────────────────────────────────────────────
tree_frame = ttk.Frame(self) tree_frame = ttk.Frame(self)
@@ -213,3 +219,54 @@ class HistoryScreen(ttk.Frame):
for c in _COLUMNS: for c in _COLUMNS:
self._tree.heading(c, text=_LABELS[c]) self._tree.heading(c, text=_LABELS[c])
self._apply_filter() self._apply_filter()
# ── Export ────────────────────────────────────────────────────────────────
def _current_filter(self):
"""Return (game_id, date_from, date_to) for the active filter."""
name = self._game_var.get()
if name == "All Games":
game_id = None
else:
g = get_game_by_name(name)
game_id = g["id"] if g else None
raw_from = self._from_entry.get().strip()
raw_to = self._to_entry.get().strip()
date_from = raw_from if raw_from and raw_from != "YYYY-MM-DD" else None
date_to = raw_to if raw_to and raw_to != "YYYY-MM-DD" else None
return game_id, date_from, date_to
def _export_excel(self):
ensure_exports_dir()
fp = filedialog.asksaveasfilename(
title="Export draw history",
defaultextension=".xlsx",
filetypes=[("Excel workbook", "*.xlsx")],
initialfile=f"draws_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.xlsx",
)
if not fp:
return
try:
game_id, date_from, date_to = self._current_filter()
export_draws_excel(fp, game_id=game_id, date_from=date_from, date_to=date_to)
self._count_var.set(f"Exported → {os.path.basename(fp)}")
except Exception as e:
messagebox.showerror("Export failed", str(e))
def _export_csv(self):
ensure_exports_dir()
fp = filedialog.asksaveasfilename(
title="Export draw history",
defaultextension=".csv",
filetypes=[("CSV file", "*.csv")],
initialfile=f"draws_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.csv",
)
if not fp:
return
try:
game_id, date_from, date_to = self._current_filter()
export_draws_csv(fp, game_id=game_id, date_from=date_from, date_to=date_to)
self._count_var.set(f"Exported → {os.path.basename(fp)}")
except Exception as e:
messagebox.showerror("Export failed", str(e))
+45 -1
View File
@@ -5,14 +5,16 @@ Prediction generator screen.
Pick a strategy + game + ticket count → generate → save to DB. Pick a strategy + game + ticket count → generate → save to DB.
""" """
import os
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk, filedialog, messagebox
import logging import logging
from db.models import get_all_games, get_game_by_name, insert_prediction from db.models import get_all_games, get_game_by_name, insert_prediction
from core.predictor import ( from core.predictor import (
hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick,
) )
from core.exporter import export_predictions_excel, export_predictions_csv, ensure_exports_dir
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -121,6 +123,14 @@ class PredictorScreen(ttk.Frame):
bottom, text="Clear", command=self._clear bottom, text="Clear", command=self._clear
).pack(side="right", padx=(0, 4)) ).pack(side="right", padx=(0, 4))
ttk.Button(
bottom, text="Export CSV", command=self._export_csv, state="normal"
).pack(side="right", padx=(0, 4))
ttk.Button(
bottom, text="Export Excel", command=self._export_excel, state="normal"
).pack(side="right", padx=(0, 4))
# ── Callbacks ───────────────────────────────────────────────────────────── # ── Callbacks ─────────────────────────────────────────────────────────────
def refresh(self): def refresh(self):
@@ -198,3 +208,37 @@ class PredictorScreen(ttk.Frame):
self._tree.delete(*self._tree.get_children()) self._tree.delete(*self._tree.get_children())
self._save_btn.config(state="disabled") self._save_btn.config(state="disabled")
self._status_var.set("") self._status_var.set("")
def _export_excel(self):
ensure_exports_dir()
fp = filedialog.asksaveasfilename(
title="Export predictions",
defaultextension=".xlsx",
filetypes=[("Excel workbook", "*.xlsx")],
initialfile=f"predictions_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.xlsx",
)
if not fp:
return
try:
game_id = self._game_id
export_predictions_excel(fp, game_id=game_id)
self._status_var.set(f"Exported → {os.path.basename(fp)}")
except Exception as e:
messagebox.showerror("Export failed", str(e))
def _export_csv(self):
ensure_exports_dir()
fp = filedialog.asksaveasfilename(
title="Export predictions",
defaultextension=".csv",
filetypes=[("CSV file", "*.csv")],
initialfile=f"predictions_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.csv",
)
if not fp:
return
try:
game_id = self._game_id
export_predictions_csv(fp, game_id=game_id)
self._status_var.set(f"Exported → {os.path.basename(fp)}")
except Exception as e:
messagebox.showerror("Export failed", str(e))