05/23 Phase 8

This commit is contained in:
2026-05-23 12:05:33 -04:00
parent 8fc1878670
commit 524cdfda9a
10 changed files with 149 additions and 18 deletions
+3 -1
View File
@@ -8,7 +8,9 @@
"Bash(python -m pytest tests/test_predictor.py -v)",
"Bash(python -m pytest)",
"Bash(python -m pytest tests/test_settings.py -v)",
"Bash(python -m pytest tests/ -v --tb=short)"
"Bash(python -m pytest tests/ -v --tb=short)",
"Bash(python -m pytest tests/ -q --tb=short)",
"Bash(python -m PyInstaller lottosight.spec --clean)"
]
}
}
+5 -3
View File
@@ -28,10 +28,12 @@ share/python-wheels/
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# *.spec intentionally NOT ignored — lottosight.spec is committed
# LottoSight runtime artifacts
data/*.db
exports/
# Installer logs
pip-log.txt
+10 -5
View File
@@ -325,11 +325,16 @@ All actions are logged to console and optionally to a log file:
---
### 🔲 Phase 8 — Packaging
- [ ] Write PyInstaller `.spec` file
- [ ] Test build on target OS (Windows / Mac / Linux)
- [ ] Bundle SQLite DB, assets, exports folder
- [ ] Test packaged app fresh install (no Python required)
### Phase 8 — Packaging
- [x] Write `core/paths.py``user_data_dir()` + `bundled_asset()` helpers (frozen-aware)
- [x] Update `db/database.py` + `core/exporter.py` to use `user_data_dir()` so DB + exports land next to the .exe when frozen
- [x] Update `main.py` to use `bundled_asset()` for icon lookup
- [x] Write `lottosight.spec` — directory build, no console, bundles `assets/`, matplotlib data, openpyxl templates, APScheduler hidden imports
- [x] Build verified: `pyinstaller lottosight.spec --clean` succeeds → `dist/LottoSight/` (~115 MB)
- [x] `assets/icon.png` confirmed present in `dist/LottoSight/_internal/assets/`
- [x] Fix `.gitignore` — un-ignore `*.spec`, add `data/*.db` + `exports/`
- [x] 165/165 tests still passing after paths refactor
- [ ] Test packaged app on a clean machine (no Python installed)
---
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

+2 -2
View File
@@ -18,9 +18,9 @@ import openpyxl
from openpyxl.styles import Alignment, Font, PatternFill
from db.models import get_draws_with_game, get_game_by_id, get_predictions
from core.paths import user_data_dir
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
EXPORTS_DIR = os.path.join(BASE_DIR, "exports")
EXPORTS_DIR = os.path.join(user_data_dir(), "exports")
def ensure_exports_dir():
+33
View File
@@ -0,0 +1,33 @@
"""
core/paths.py
-------------
Runtime path resolution that works both in development and when
frozen by PyInstaller (--onefile or directory build).
user_data_dir() mutable data root (DB, exports) next to the .exe
bundled_asset(rel) read-only asset path (icon etc.) extracted bundle
"""
import os
import sys
def user_data_dir() -> str:
"""Root for mutable user data. Next to the .exe when frozen."""
if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable)
# development: two levels up from core/paths.py → project root
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def bundled_asset(relative_path: str) -> str:
"""Absolute path to a read-only bundled asset (icon, etc.).
In a onefile build sys._MEIPASS is the temp extraction dir;
in a directory build it equals the exe directory.
"""
if getattr(sys, "frozen", False):
base = sys._MEIPASS # type: ignore[attr-defined]
else:
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, relative_path)
Binary file not shown.
+3 -3
View File
@@ -9,11 +9,11 @@ import sqlite3
import logging
import os
from core.paths import user_data_dir
logger = logging.getLogger(__name__)
# DB file lives in lottosight/data/lottosight.db
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DB_PATH = os.path.join(BASE_DIR, "data", "lottosight.db")
DB_PATH = os.path.join(user_data_dir(), "data", "lottosight.db")
def get_connection():
+87
View File
@@ -0,0 +1,87 @@
# lottosight.spec
# ----------------
# PyInstaller spec for LottoSight (Windows directory build).
# Build: pyinstaller lottosight.spec
# Output: dist/LottoSight/LottoSight.exe (+ supporting files)
#
# User-writable data (DB, exports) is stored next to the .exe at runtime.
# Read-only assets (icon) are bundled inside the package via datas.
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
# ── Data files ────────────────────────────────────────────────────────────────
datas = [
# Bundle the assets folder (icon.png) into the package root
('assets', 'assets'),
]
# Matplotlib ships its own data directory (fonts, colormaps, style sheets)
datas += collect_data_files('matplotlib')
# openpyxl ships its own templates
datas += collect_data_files('openpyxl')
# ── Hidden imports ────────────────────────────────────────────────────────────
hiddenimports = [
# Matplotlib TkAgg backend — not detected by static analysis
'matplotlib.backends.backend_tkagg',
# APScheduler components loaded via entry-point discovery at runtime
'apscheduler.schedulers.background',
'apscheduler.executors.pool',
'apscheduler.jobstores.memory',
'apscheduler.triggers.interval',
'apscheduler.triggers.date',
# bs4 HTML parser back-end
'bs4.builder._htmlparser',
# Tkinter sub-modules
'tkinter',
'tkinter.ttk',
'tkinter.filedialog',
'tkinter.messagebox',
]
# ── Analysis ──────────────────────────────────────────────────────────────────
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
# Strip test / notebook dependencies to keep the bundle smaller
excludes=['pytest', 'IPython', 'ipykernel', 'jupyter'],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
# ── Executable ────────────────────────────────────────────────────────────────
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True, # binaries go into COLLECT, not baked into exe
name='LottoSight',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False, # no console window (pure GUI app)
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
# ── Directory bundle ──────────────────────────────────────────────────────────
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='LottoSight', # output: dist/LottoSight/
)
+6 -4
View File
@@ -17,6 +17,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
from db.database import init_db
from core.fetcher import fetch_all
from core.exporter import create_icon_png
from core.paths import bundled_asset, user_data_dir
from ui.statusbar import StatusBar
from ui.history import HistoryScreen
from ui.analysis import AnalysisScreen
@@ -38,7 +39,7 @@ class LottoSightApp(tk.Tk):
self.minsize(900, 600)
try:
self.iconphoto(True, tk.PhotoImage(file="assets/icon.png"))
self.iconphoto(True, tk.PhotoImage(file=bundled_asset("assets/icon.png")))
except Exception:
pass
@@ -172,9 +173,10 @@ class LottoSightApp(tk.Tk):
def main():
init_db()
if not os.path.exists("assets/icon.png"):
os.makedirs("assets", exist_ok=True)
create_icon_png("assets/icon.png")
icon_path = bundled_asset("assets/icon.png")
if not os.path.exists(icon_path):
os.makedirs(os.path.dirname(icon_path), exist_ok=True)
create_icon_png(icon_path)
app = LottoSightApp()
app.protocol("WM_DELETE_WINDOW", app.on_close)
app.mainloop()