add update notification
This commit is contained in:
parent
f3b78400d4
commit
5fac4cd9e8
8 changed files with 122 additions and 5 deletions
|
|
@ -15,7 +15,7 @@ decoder-only offsets.
|
||||||
"capture_source": "npcap",
|
"capture_source": "npcap",
|
||||||
"exporter": {
|
"exporter": {
|
||||||
"name": "nte-history-exporter",
|
"name": "nte-history-exporter",
|
||||||
"version": "0.1.6"
|
"version": "0.1.7"
|
||||||
},
|
},
|
||||||
"banner": {
|
"banner": {
|
||||||
"id": "Lottery_Permanent",
|
"id": "Lottery_Permanent",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "nte-history-exporter"
|
name = "nte-history-exporter"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
description = "Cross-platform Neverness to Everness pull-history exporter."
|
description = "Cross-platform Neverness to Everness pull-history exporter."
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.6"
|
__version__ = "0.1.7"
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ import json
|
||||||
|
|
||||||
from nte_history_exporter import console
|
from nte_history_exporter import console
|
||||||
from nte_history_exporter.adapters.mitmproxy_flows import decode_mitmproxy_flows
|
from nte_history_exporter.adapters.mitmproxy_flows import decode_mitmproxy_flows
|
||||||
|
from nte_history_exporter.constants import EXPORTER_VERSION
|
||||||
from nte_history_exporter.decoder.boundary import annotate_groups
|
from nte_history_exporter.decoder.boundary import annotate_groups
|
||||||
from nte_history_exporter.export.csv_export import write_csv
|
from nte_history_exporter.export.csv_export import write_csv
|
||||||
from nte_history_exporter.export.json_export import build_export_json
|
from nte_history_exporter.export.json_export import build_export_json
|
||||||
from nte_history_exporter.live_capture.libpcap import LibpcapUnavailable
|
from nte_history_exporter.live_capture.libpcap import LibpcapUnavailable
|
||||||
from nte_history_exporter.live_capture.runner import export_paths, run_live_capture
|
from nte_history_exporter.live_capture.runner import export_paths, run_live_capture
|
||||||
|
from nte_history_exporter.update_check import check_for_update
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
|
@ -38,6 +40,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
args = build_parser().parse_args(argv)
|
args = build_parser().parse_args(argv)
|
||||||
console.print_banner()
|
console.print_banner()
|
||||||
|
update = check_for_update(EXPORTER_VERSION)
|
||||||
|
if update:
|
||||||
|
console.print_update_available(update.current_version, update.latest_version, update.release_url)
|
||||||
if args.live or not args.capture_source:
|
if args.live or not args.capture_source:
|
||||||
try:
|
try:
|
||||||
run_live_capture(
|
run_live_capture(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ BOLD = "\x1b[1m"
|
||||||
DIM = "\x1b[2m"
|
DIM = "\x1b[2m"
|
||||||
CYAN = "\x1b[36m"
|
CYAN = "\x1b[36m"
|
||||||
GREEN = "\x1b[32m"
|
GREEN = "\x1b[32m"
|
||||||
|
RED = "\x1b[31m"
|
||||||
YELLOW = "\x1b[33m"
|
YELLOW = "\x1b[33m"
|
||||||
BRIGHT_WHITE = "\x1b[97m"
|
BRIGHT_WHITE = "\x1b[97m"
|
||||||
|
|
||||||
|
|
@ -62,7 +63,7 @@ def print_banner() -> None:
|
||||||
print(style(f" {GAME_NAME} pull history -> tracker JSON", DIM))
|
print(style(f" {GAME_NAME} pull history -> tracker JSON", DIM))
|
||||||
print(style(f" {REPOSITORY_URL}", DIM))
|
print(style(f" {REPOSITORY_URL}", DIM))
|
||||||
heart = "❤️" if (getattr(sys.stdout, "encoding", None) or "").lower().replace("-", "") == "utf8" else "<3"
|
heart = "❤️" if (getattr(sys.stdout, "encoding", None) or "").lower().replace("-", "") == "utf8" else "<3"
|
||||||
print(style(f" Created with {heart} by Golumpa", DIM))
|
print(style(" Created with ", DIM) + style(heart, RED) + style(" by Golumpa", DIM))
|
||||||
print(rule("="))
|
print(rule("="))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -162,6 +163,14 @@ def print_problem(text: str) -> None:
|
||||||
print(style(f" {text}", YELLOW, BOLD))
|
print(style(f" {text}", YELLOW, BOLD))
|
||||||
|
|
||||||
|
|
||||||
|
def print_update_available(current_version: str, latest_version: str, release_url: str) -> None:
|
||||||
|
print()
|
||||||
|
print(style(f" Update available: v{current_version} -> {latest_version}", YELLOW, BOLD))
|
||||||
|
print(style(" Some banner/reward mappings may be incomplete in this version.", YELLOW))
|
||||||
|
print(style(" Updating is recommended for the most accurate export labels.", YELLOW))
|
||||||
|
print(style(f" Download when ready: {release_url}", YELLOW))
|
||||||
|
|
||||||
|
|
||||||
def prompt_user_uid() -> str | None:
|
def prompt_user_uid() -> str | None:
|
||||||
print()
|
print()
|
||||||
print_problem("User UID was not detected in this capture.")
|
print_problem("User UID was not detected in this capture.")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ LIMITED_CHARACTER_BANNER_NAME = "Limited Character Board"
|
||||||
ARC_BANNER_ID = "Arc_MiracleBox"
|
ARC_BANNER_ID = "Arc_MiracleBox"
|
||||||
ARC_BANNER_NAME = "Arc Miracle Box"
|
ARC_BANNER_NAME = "Arc Miracle Box"
|
||||||
EXPORTER_NAME = "nte-history-exporter"
|
EXPORTER_NAME = "nte-history-exporter"
|
||||||
EXPORTER_VERSION = "0.1.6"
|
EXPORTER_VERSION = "0.1.5"
|
||||||
|
|
||||||
HISTORY_REQUEST_BANNER = 4220
|
HISTORY_REQUEST_BANNER = 4220
|
||||||
HISTORY_REQUEST_LENGTH = 45
|
HISTORY_REQUEST_LENGTH = 45
|
||||||
|
|
|
||||||
70
src/nte_history_exporter/update_check.py
Normal file
70
src/nte_history_exporter/update_check.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
LATEST_RELEASE_API_URL = "https://api.github.com/repos/Golumpa/nte-exporter/releases/latest"
|
||||||
|
RELEASES_URL = "https://github.com/Golumpa/nte-exporter/releases"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class UpdateInfo:
|
||||||
|
current_version: str
|
||||||
|
latest_version: str
|
||||||
|
release_url: str
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_update(current_version: str, *, timeout: float = 1.5) -> UpdateInfo | None:
|
||||||
|
"""Return update details when GitHub has a newer release.
|
||||||
|
|
||||||
|
Update checks are intentionally best-effort. Network errors, invalid
|
||||||
|
responses, or unusual tag names should not stop the exporter from running.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
latest = fetch_latest_release(timeout=timeout)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
latest_version = str(latest.get("tag_name", "")).strip()
|
||||||
|
release_url = str(latest.get("html_url", "")).strip() or RELEASES_URL
|
||||||
|
if not latest_version or not is_newer_version(latest_version, current_version):
|
||||||
|
return None
|
||||||
|
return UpdateInfo(
|
||||||
|
current_version=current_version,
|
||||||
|
latest_version=latest_version,
|
||||||
|
release_url=release_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_latest_release(*, timeout: float) -> dict[str, Any]:
|
||||||
|
request = urllib.request.Request(
|
||||||
|
LATEST_RELEASE_API_URL,
|
||||||
|
headers={
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"User-Agent": "nte-history-exporter",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||||
|
return json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_newer_version(candidate: str, current: str) -> bool:
|
||||||
|
candidate_parts = _version_parts(candidate)
|
||||||
|
current_parts = _version_parts(current)
|
||||||
|
if candidate_parts is None or current_parts is None:
|
||||||
|
return False
|
||||||
|
width = max(len(candidate_parts), len(current_parts))
|
||||||
|
normalized_candidate = candidate_parts + (0,) * (width - len(candidate_parts))
|
||||||
|
normalized_current = current_parts + (0,) * (width - len(current_parts))
|
||||||
|
return normalized_candidate > normalized_current
|
||||||
|
|
||||||
|
|
||||||
|
def _version_parts(version: str) -> tuple[int, ...] | None:
|
||||||
|
match = re.fullmatch(r"v?(\d+(?:\.\d+)*)", version.strip())
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
return tuple(int(part) for part in match.group(1).split("."))
|
||||||
|
|
@ -49,6 +49,7 @@ from nte_history_exporter.live_capture.backends import open_capture_backend
|
||||||
from nte_history_exporter.export.json_export import build_export_json
|
from nte_history_exporter.export.json_export import build_export_json
|
||||||
from nte_history_exporter.live_capture.runner import export_paths
|
from nte_history_exporter.live_capture.runner import export_paths
|
||||||
from nte_history_exporter.pool_mappings import load_pool_mappings, pool_meta_from_mapping
|
from nte_history_exporter.pool_mappings import load_pool_mappings, pool_meta_from_mapping
|
||||||
|
from nte_history_exporter.update_check import UpdateInfo, check_for_update, is_newer_version
|
||||||
|
|
||||||
|
|
||||||
def load_reference_csv(name):
|
def load_reference_csv(name):
|
||||||
|
|
@ -199,6 +200,38 @@ class BoundaryExportTests(unittest.TestCase):
|
||||||
with self.subTest(pool_key=pool_key):
|
with self.subTest(pool_key=pool_key):
|
||||||
self.assertEqual(pool_meta_from_mapping(mapping), POOL_META[pool_key])
|
self.assertEqual(pool_meta_from_mapping(mapping), POOL_META[pool_key])
|
||||||
|
|
||||||
|
def test_update_version_comparison_handles_release_tags(self):
|
||||||
|
self.assertTrue(is_newer_version("v0.1.7", "0.1.6"))
|
||||||
|
self.assertTrue(is_newer_version("0.2.0", "0.1.6"))
|
||||||
|
self.assertTrue(is_newer_version("v0.1.10", "0.1.9"))
|
||||||
|
self.assertFalse(is_newer_version("v0.1.6", "0.1.6"))
|
||||||
|
self.assertFalse(is_newer_version("v0.1.5", "0.1.6"))
|
||||||
|
self.assertFalse(is_newer_version("latest", "0.1.6"))
|
||||||
|
|
||||||
|
def test_update_check_reports_newer_github_release(self):
|
||||||
|
latest = {
|
||||||
|
"tag_name": "v0.1.7",
|
||||||
|
"html_url": "https://github.com/Golumpa/nte-exporter/releases/tag/v0.1.7",
|
||||||
|
}
|
||||||
|
with patch("nte_history_exporter.update_check.fetch_latest_release", return_value=latest):
|
||||||
|
update = check_for_update("0.1.6", timeout=0.1)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
update,
|
||||||
|
UpdateInfo(
|
||||||
|
current_version="0.1.6",
|
||||||
|
latest_version="v0.1.7",
|
||||||
|
release_url="https://github.com/Golumpa/nte-exporter/releases/tag/v0.1.7",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_check_is_quiet_when_unavailable_or_current(self):
|
||||||
|
with patch("nte_history_exporter.update_check.fetch_latest_release", side_effect=OSError("offline")):
|
||||||
|
self.assertIsNone(check_for_update("0.1.6", timeout=0.1))
|
||||||
|
|
||||||
|
with patch("nte_history_exporter.update_check.fetch_latest_release", return_value={"tag_name": "v0.1.6"}):
|
||||||
|
self.assertIsNone(check_for_update("0.1.6", timeout=0.1))
|
||||||
|
|
||||||
def test_reward_mapping_files_have_expected_shape(self):
|
def test_reward_mapping_files_have_expected_shape(self):
|
||||||
self.assertTrue(ARC_META)
|
self.assertTrue(ARC_META)
|
||||||
for arc_id, meta in ARC_META.items():
|
for arc_id, meta in ARC_META.items():
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue