122 lines
3.7 KiB
Python
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
|