from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta from pathlib import Path import re _VERSION_PATTERN = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$") _FEATURE_UPDATED_AT_PATTERN = re.compile( r"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]\+08:00$" ) @dataclass(frozen=True, order=True) class SoftwareVersion: major: int minor: int patch: int @classmethod def parse(cls, value: str) -> "SoftwareVersion": if not isinstance(value, str): raise ValueError("software version must be a string") match = _VERSION_PATTERN.fullmatch(value.strip()) if match is None: raise ValueError("software version must use strict MAJOR.MINOR.PATCH digits") return cls(*(int(part) for part in match.groups())) def __str__(self) -> str: return f"{self.major}.{self.minor}.{self.patch}" def read_software_version(code_root: Path) -> SoftwareVersion: path = Path(code_root) / "VERSION" try: return SoftwareVersion.parse(path.read_text(encoding="utf-8")) except OSError as exc: raise RuntimeError(f"software VERSION file is unreadable: {path}") from exc def read_feature_updated_at(code_root: Path) -> str: path = Path(code_root) / "FEATURE_UPDATED_AT" try: value = path.read_text(encoding="utf-8").strip() except OSError as exc: raise RuntimeError(f"feature update timestamp file is unreadable: {path}") from exc if _FEATURE_UPDATED_AT_PATTERN.fullmatch(value) is None: raise ValueError("feature update timestamp must use YYYY-MM-DDTHH:MM+08:00") try: parsed = datetime.fromisoformat(value) except ValueError as exc: raise ValueError("feature update timestamp is not a valid calendar time") from exc if parsed.utcoffset() != timedelta(hours=8): raise ValueError("feature update timestamp must use Beijing UTC+08:00") return value