diff --git a/.gitignore b/.gitignore index 003b816..273f648 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,21 @@ .bin/ data-dev/ + +# Local deployment/config secrets +.env +docker-compose.override.yml +example/autoupdate.json + +# Python +__pycache__/ +*.py[cod] + +# macOS +.DS_Store +.AppleDouble +.LSOverride +Icon? +._* +.Spotlight-V100 +.Trashes +.fseventsd diff --git a/example/autoupdate.py b/example/autoupdate.py index 2fcb4a4..07f297f 100644 --- a/example/autoupdate.py +++ b/example/autoupdate.py @@ -21,6 +21,12 @@ class UpdateError(RuntimeError): ReleaseHandler = Callable[[Path, dict[str, Any]], None] +DEFAULT_CONFIG_NAME = "autoupdate.json" +DEFAULT_CONFIG = { + "base_url": "https://updates.example.com", + "api_key": "upsk_replace_me", + "project_slug": "example-project", +} def ensure_updated( @@ -37,7 +43,17 @@ def ensure_updated( If `apply_update` is provided, it receives `(downloaded_artifact_path, release_dict)` and is responsible for applying the downloaded artifact before restart. """ - if not api_key or api_key == "upsk_replace_me": + if _is_placeholder_config( + { + "base_url": base_url, + "api_key": api_key, + "project_slug": project_slug, + } + ): + print("autoupdate: configure autoupdate.json to enable updates") + return False + + if not api_key: raise UpdateError("set a real API key before calling ensure_updated") script_path = _current_script_path() @@ -51,6 +67,8 @@ def ensure_updated( if not latest_version or latest_version == current_version: return False + print(f"autoupdate: updating {current_version} -> {latest_version}") + download_url = urljoin(base_url.rstrip("/") + "/", str(release.get("download_url") or "")) if not download_url: raise UpdateError("latest release does not contain a download URL") @@ -75,6 +93,27 @@ def ensure_updated( return True +def ensure_updated_from_config( + current_version: str, + config_path: str | Path | None = None, + apply_update: ReleaseHandler | None = None, +) -> bool: + """ + Load update settings from JSON and run the update check. + + If `config_path` is omitted, `autoupdate.json` is loaded from the same + directory as the currently running script. + """ + config = _load_config(config_path) + return ensure_updated( + base_url=config["base_url"], + api_key=config["api_key"], + project_slug=config["project_slug"], + current_version=current_version, + apply_update=apply_update, + ) + + def _current_script_path() -> Path: main_module = sys.modules.get("__main__") main_file = getattr(main_module, "__file__", None) @@ -83,6 +122,52 @@ def _current_script_path() -> Path: return Path(main_file).resolve() +def _default_config_path() -> Path: + return _current_script_path().with_name(DEFAULT_CONFIG_NAME) + + +def _load_config(config_path: str | Path | None) -> dict[str, str]: + path = ( + Path(config_path).expanduser() + if config_path is not None + else _default_config_path() + ) + try: + with path.open("r", encoding="utf-8") as config_file: + payload = json.load(config_file) + except FileNotFoundError: + _write_default_config(path) + return DEFAULT_CONFIG.copy() + except json.JSONDecodeError as exc: + raise UpdateError(f"update config file is not valid JSON: {path}") from exc + + if not isinstance(payload, dict): + raise UpdateError(f"update config must be a JSON object: {path}") + + config: dict[str, str] = {} + for key in ("base_url", "api_key", "project_slug"): + value = payload.get(key) + if not isinstance(value, str) or not value.strip(): + raise UpdateError(f"update config field {key!r} is required: {path}") + config[key] = value.strip() + + return config + + +def _write_default_config(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as config_file: + json.dump(DEFAULT_CONFIG, config_file, indent=2) + config_file.write("\n") + + +def _is_placeholder_config(config: dict[str, str]) -> bool: + return any( + config.get(key, "").strip() == value + for key, value in DEFAULT_CONFIG.items() + ) + + def _make_request(url: str, api_key: str, accept_json: bool = False) -> Request: headers = { "Authorization": f"Bearer {api_key}", diff --git a/example/update_client.py b/example/update_client.py index 3bf6b3d..dbc0237 100644 --- a/example/update_client.py +++ b/example/update_client.py @@ -1,12 +1,7 @@ -from autoupdate import ensure_updated +from autoupdate import ensure_updated_from_config -ensure_updated( - base_url="http://127.0.0.1:8080", - api_key="upsk_zdQu4gosIJjvIaQfn5A_ux9msFyDfEnc8c29F0ZCTmk", - project_slug="test", - current_version="0.1.3", -) +ver = "0.1.1" +ensure_updated_from_config(current_version=ver) - -print("TEst") +print(f"TEst {ver}")