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

1"""GitHub Releases で新しい版があるかを見る。公開 API への匿名の GET だけで、利用者を特定できるものは送らない。""" 

2import json 

3import re 

4import urllib.error 

5import urllib.request 

6from typing import Any 

7 

8GITHUB_REPO = "STsuruga/Graphica" 

9_RELEASES_LATEST_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest" 

10_DEFAULT_TIMEOUT_SEC = 5 

11 

12 

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,) 

17 

18 

19def is_newer_version(remote_version: str, local_version: str) -> bool: 

20 return _parse_version(remote_version) > _parse_version(local_version) 

21 

22 

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 } 

32 

33 

34def fetch_latest_release_info(timeout: float = _DEFAULT_TIMEOUT_SEC) -> dict[str, Any]: 

35 """最新版の {'tag_name', 'html_url', 'name', 'body'(リリースノート), 'assets'(添付ファイル)}。 

36 

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 } 

52 

53 

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} 

60 

61 

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 

71 

72 

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