Files
notify-bridge/packages/server/src/notify_bridge_server/version.py
T
alexei.dolgolyov ba199f24bd feat: deferred dispatch, release-check provider, settings polish
- Defer quiet-hours dispatches into new deferred_dispatch table; drain
  job + periodic catch-up scan re-fire at window end with coalescing on
  (link, event_type, collection_id).
- Add ON DELETE SET NULL migration on event_log_id and partial unique
  index on (link_id, collection_id, event_type) WHERE status='pending'.
- Add release-check provider abstraction (Gitea/GitHub) with SSRF-safe
  URL validation, settings UI cassette, and scheduled polling.
- Replace importlib-only version lookup with version.py helper that
  prefers the higher of installed metadata vs source pyproject so stale
  editable dev installs stop misreporting.
- Aurora frontend polish: MetaStrip component, ReleaseCassette,
  EventDetailModal expansion, and i18n additions.
2026-05-12 02:58:07 +03:00

84 lines
3.1 KiB
Python

"""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