05/23 Phase 8
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 243 B |
+2
-2
@@ -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():
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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/
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user