05/23 Phase 7
This commit is contained in:
@@ -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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user