""" 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