Refine autoupdate config handling
This commit is contained in:
parent
bcef2d7b52
commit
71598cda7d
3 changed files with 109 additions and 10 deletions
19
.gitignore
vendored
19
.gitignore
vendored
|
|
@ -1,2 +1,21 @@
|
||||||
.bin/
|
.bin/
|
||||||
data-dev/
|
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
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,12 @@ class UpdateError(RuntimeError):
|
||||||
|
|
||||||
|
|
||||||
ReleaseHandler = Callable[[Path, dict[str, Any]], None]
|
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(
|
def ensure_updated(
|
||||||
|
|
@ -37,7 +43,17 @@ def ensure_updated(
|
||||||
If `apply_update` is provided, it receives `(downloaded_artifact_path, release_dict)`
|
If `apply_update` is provided, it receives `(downloaded_artifact_path, release_dict)`
|
||||||
and is responsible for applying the downloaded artifact before restart.
|
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")
|
raise UpdateError("set a real API key before calling ensure_updated")
|
||||||
|
|
||||||
script_path = _current_script_path()
|
script_path = _current_script_path()
|
||||||
|
|
@ -51,6 +67,8 @@ def ensure_updated(
|
||||||
if not latest_version or latest_version == current_version:
|
if not latest_version or latest_version == current_version:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
print(f"autoupdate: updating {current_version} -> {latest_version}")
|
||||||
|
|
||||||
download_url = urljoin(base_url.rstrip("/") + "/", str(release.get("download_url") or ""))
|
download_url = urljoin(base_url.rstrip("/") + "/", str(release.get("download_url") or ""))
|
||||||
if not download_url:
|
if not download_url:
|
||||||
raise UpdateError("latest release does not contain a download URL")
|
raise UpdateError("latest release does not contain a download URL")
|
||||||
|
|
@ -75,6 +93,27 @@ def ensure_updated(
|
||||||
return True
|
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:
|
def _current_script_path() -> Path:
|
||||||
main_module = sys.modules.get("__main__")
|
main_module = sys.modules.get("__main__")
|
||||||
main_file = getattr(main_module, "__file__", None)
|
main_file = getattr(main_module, "__file__", None)
|
||||||
|
|
@ -83,6 +122,52 @@ def _current_script_path() -> Path:
|
||||||
return Path(main_file).resolve()
|
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:
|
def _make_request(url: str, api_key: str, accept_json: bool = False) -> Request:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {api_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
from autoupdate import ensure_updated
|
from autoupdate import ensure_updated_from_config
|
||||||
|
|
||||||
ensure_updated(
|
ver = "0.1.1"
|
||||||
base_url="http://127.0.0.1:8080",
|
|
||||||
api_key="upsk_zdQu4gosIJjvIaQfn5A_ux9msFyDfEnc8c29F0ZCTmk",
|
|
||||||
project_slug="test",
|
|
||||||
current_version="0.1.3",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
ensure_updated_from_config(current_version=ver)
|
||||||
|
|
||||||
|
print(f"TEst {ver}")
|
||||||
print("TEst")
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue