ba199f24bd
- 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.
84 lines
3.1 KiB
Python
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
|