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.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user