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