Stock-Tool/updater.py

122 lines
3.7 KiB
Python

"""
Auto-Updater
============
Checks Supabase for a newer version of screener_gui.py and stock_screener.py.
If found, silently downloads and replaces the files in the exe directory,
then restarts the application.
Called after a successful license check on every launch.
"""
import os
import sys
import tempfile
import shutil
import requests
# ---------------------------------------------------------------------------
# Current app version — bump this in lockstep with new Supabase version rows
# ---------------------------------------------------------------------------
APP_VERSION = "1.0.0"
# ---------------------------------------------------------------------------
# Supabase config (same anon key as license_check.py)
# ---------------------------------------------------------------------------
_SUPABASE_URL = "https://yeispcpmepjelfbhfkwr.supabase.co"
_SUPABASE_ANON = (
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InllaXNwY3BtZXBqZWxmYmhma3dyIiwi"
"cm9sZSI6ImFub24iLCJpYXQiOjE3NzM1MDkxOTAsImV4cCI6MjA4OTA4NTE5MH0."
"8kq6HzpwF81_Sx36MXbx0wUqaT9Ul9POw1LzF5vxiOo"
)
_HEADERS = {
"apikey": _SUPABASE_ANON,
"Content-Type": "application/json",
}
# Files that can be updated without rebuilding the exe
_UPDATABLE_FILES = ("screener_gui.py", "stock_screener.py")
def _exe_dir() -> str:
if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
def _parse_version(v: str) -> tuple:
"""Convert '1.2.3' → (1, 2, 3) for comparison."""
try:
return tuple(int(x) for x in v.strip().split("."))
except Exception:
return (0, 0, 0)
def _download_file(url: str, dest_path: str) -> None:
"""Download url → dest_path, using a temp file so the write is atomic."""
tmp_dir = tempfile.mkdtemp()
tmp_path = os.path.join(tmp_dir, os.path.basename(dest_path))
try:
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
with open(tmp_path, "wb") as f:
for chunk in r.iter_content(chunk_size=65536):
f.write(chunk)
shutil.move(tmp_path, dest_path)
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
def _restart() -> None:
"""Restart the current process cleanly."""
if getattr(sys, "frozen", False):
os.execv(sys.executable, [sys.executable] + sys.argv[1:])
else:
os.execv(sys.executable, [sys.executable] + sys.argv)
def check_and_apply_update() -> None:
"""
Silently check for updates and apply them if available.
Errors are swallowed — a failed update check never blocks the app.
"""
try:
resp = requests.post(
f"{_SUPABASE_URL}/rest/v1/rpc/get_latest_version",
headers=_HEADERS,
json={},
timeout=8,
)
resp.raise_for_status()
data = resp.json()
latest = data.get("version", APP_VERSION)
gui_url = data.get("gui_py_url")
scr_url = data.get("screener_py_url")
if _parse_version(latest) <= _parse_version(APP_VERSION):
return # already up to date
exe_dir = _exe_dir()
updated = False
if gui_url:
dest = os.path.join(exe_dir, "screener_gui.py")
_download_file(gui_url, dest)
updated = True
if scr_url:
dest = os.path.join(exe_dir, "stock_screener.py")
_download_file(scr_url, dest)
updated = True
if updated:
# Restart so the fresh .py files are loaded
_restart()
except Exception:
# Never crash the app over a failed update check
pass