"""Server version resolution. Production Docker images install the wheel and ``importlib.metadata`` is the truth. Editable dev installs (``pip install -e packages/server``) record the version at install time and *don't auto-refresh* when the source ``pyproject.toml`` bumps — so a developer that bumped from 0.3.x to 0.7.x without reinstalling will keep reporting 0.3.x via ``importlib.metadata``. To make the running app match the source tree without forcing a reinstall, we read both and return the higher of the two. The dist-info wins in prod (no pyproject alongside), the source wins in dev when the editable install is stale. """ from __future__ import annotations import logging from importlib.metadata import PackageNotFoundError, version as _pkg_version from pathlib import Path _LOGGER = logging.getLogger(__name__) _PACKAGE_NAME = "notify-bridge-server" _UNKNOWN = "0.0.0+unknown" def _read_source_version() -> str | None: """Best-effort read of the source ``pyproject.toml`` version. Returns ``None`` when the file isn't reachable (the normal prod case), so callers fall back to the installed metadata. """ # Module is at packages/server/src/notify_bridge_server/version.py, # pyproject sits at packages/server/pyproject.toml — three parents up. pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml" if not pyproject.is_file(): return None try: import tomllib # Python 3.11+ stdlib — server requires 3.12. data = tomllib.loads(pyproject.read_text(encoding="utf-8")) version = data.get("project", {}).get("version") return str(version) if version else None except (OSError, ValueError) as err: # pragma: no cover — defensive _LOGGER.debug("Could not read source pyproject version: %s", err) return None def _segments(version: str) -> tuple[int, ...]: """Best-effort tuple-of-ints for ordering. Suffixes (``-rc1``) are stripped.""" if not version: return () head = version.split("+", 1)[0].split("-", 1)[0] out: list[int] = [] for piece in head.split("."): digits = "".join(c for c in piece if c.isdigit()) if digits: out.append(int(digits)) return tuple(out) def resolve_version() -> str: """Return the version the running server should advertise. Prefers the highest of (installed metadata, source pyproject) so an out-of-date editable install never lies to the UI. In production builds only the installed metadata is available, which is correct by definition. """ try: installed: str | None = _pkg_version(_PACKAGE_NAME) except PackageNotFoundError: installed = None source = _read_source_version() candidates = [v for v in (installed, source) if v] if not candidates: return _UNKNOWN if len(candidates) == 1: return candidates[0] # Two candidates — return the higher by numeric segments. Ties: prefer # source, since that's what the developer just edited. a, b = candidates return a if _segments(a) > _segments(b) else b