Coverage for graphica/core/update_check.py: 100.0%
35 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""GitHub Releases で新しい版があるかを見る。公開 API への匿名の GET だけで、利用者を特定できるものは送らない。"""
2import json
3import re
4import urllib.error
5import urllib.request
6from typing import Any
8GITHUB_REPO = "STsuruga/Graphica"
9_RELEASES_LATEST_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
10_DEFAULT_TIMEOUT_SEC = 5
13def _parse_version(version_str: str) -> tuple[int, ...]:
14 """'v1.3.5' などを比べられる整数のタプルにする(数字以外は無視)。"""
15 numbers = re.findall(r'\d+', version_str or '')
16 return tuple(int(n) for n in numbers) if numbers else (0,)
19def is_newer_version(remote_version: str, local_version: str) -> bool:
20 return _parse_version(remote_version) > _parse_version(local_version)
23def _asset_info(asset: dict[str, Any]) -> dict[str, Any]:
24 digest = asset.get('digest') or ''
25 return {
26 'name': asset.get('name', '') or '',
27 'url': asset.get('browser_download_url', '') or '',
28 'size': int(asset.get('size') or 0),
29 # GitHub が添付ファイルに付ける 'sha256:<16進>'。無い(古い)添付ファイルでは空
30 'sha256': digest.split(':', 1)[1].lower() if digest.startswith('sha256:') else '',
31 }
34def fetch_latest_release_info(timeout: float = _DEFAULT_TIMEOUT_SEC) -> dict[str, Any]:
35 """最新版の {'tag_name', 'html_url', 'name', 'body'(リリースノート), 'assets'(添付ファイル)}。
37 通信や解釈の失敗は例外のまま(呼び出し側で捕まえる)。
38 """
39 request = urllib.request.Request(
40 _RELEASES_LATEST_URL,
41 headers={'Accept': 'application/vnd.github+json', 'User-Agent': 'Graphica-update-check'},
42 )
43 with urllib.request.urlopen(request, timeout=timeout) as response:
44 data = json.loads(response.read().decode('utf-8'))
45 return {
46 'tag_name': data.get('tag_name', '') or '',
47 'html_url': data.get('html_url') or f'https://github.com/{GITHUB_REPO}/releases',
48 'name': data.get('name', '') or '',
49 'body': data.get('body', '') or '',
50 'assets': [_asset_info(asset) for asset in data.get('assets') or [] if isinstance(asset, dict)],
51 }
54# 配布の形 -> 更新に使う添付ファイルの名前の条件(リリースに付ける名前は docs/dev/RELEASE_CHECKLIST.md)
55_ASSET_MATCHERS = {
56 'installer': lambda name: name.startswith('Graphica-') and name.endswith('-setup.exe'),
57 'windows_zip': lambda name: name == 'Graphica-windows.zip',
58 'macos': lambda name: name == 'Graphica-macos.zip',
59}
62def pick_asset(update_info: dict[str, Any], install_kind: str) -> dict[str, Any] | None:
63 """その配布の形の更新に使う添付ファイル。無ければ None(ダウンロードページを案内する)。"""
64 matches = _ASSET_MATCHERS.get(install_kind)
65 if matches is None:
66 return None
67 for asset in update_info.get('assets') or []:
68 if asset.get('url') and matches(asset.get('name', '')):
69 return asset
70 return None
73def check_for_update(current_version: str, timeout: float = _DEFAULT_TIMEOUT_SEC, **_ignored: Any) -> dict[str, Any] | None:
74 """TaskRunner 用。新しい版があれば fetch_latest_release_info() の辞書、無ければ None。"""
75 info = fetch_latest_release_info(timeout=timeout)
76 if info['tag_name'] and is_newer_version(info['tag_name'], current_version):
77 return info
78 return None