Source code for httk.serve.web.resources

"""Lifecycle management for resources owned by a site engine."""

from collections.abc import Callable
from threading import RLock
from types import TracebackType
from typing import Literal, Self

[docs] SITE_RESOURCES_KEY = "httk_serve_resources"
"""Stable ``global_data`` key through which site startup code gets resources."""
[docs] type CleanupCallback = Callable[[], None]
[docs] class SiteResources: """Callbacks owned by one :class:`httk.serve.web.engine.site_engine.SiteEngine`. Site ``functions/init.py`` code can obtain this object from ``global_data[SITE_RESOURCES_KEY]`` and call :meth:`register` for every persistent resource it opens. Callbacks run in reverse registration order when the :class:`httk.serve.web.engine.site_engine.SiteEngine` closes. Closing is idempotent, so ASGI lifespan shutdown and an enclosing helper may both safely close the same engine. """ def __init__(self) -> None: self._callbacks: list[CleanupCallback] = [] self._closed = False self._lock = RLock() @property
[docs] def closed(self) -> bool: """Whether cleanup has started for this registry.""" with self._lock: return self._closed
[docs] def register(self, callback: CleanupCallback) -> None: """Register one synchronous callback to run during :meth:`close`. Registration after close is a programming error: the callback could no longer be guaranteed to run. """ if not callable(callback): raise TypeError("Site resource cleanup callback must be callable") with self._lock: if self._closed: raise RuntimeError("Cannot register a site resource after the engine has closed") self._callbacks.append(callback)
[docs] def close(self) -> None: """Run pending callbacks in LIFO order exactly once. Every callback gets a chance to run. If cleanup fails, the first exception is re-raised after the remaining callbacks have executed; additional failures are attached as notes for diagnosis. """ with self._lock: if self._closed: return self._closed = True callbacks = tuple(reversed(self._callbacks)) self._callbacks.clear() errors: list[BaseException] = [] for callback in callbacks: try: callback() except BaseException as exc: errors.append(exc) if errors: primary = errors[0] for error in errors[1:]: primary.add_note(f"Additional site resource cleanup failure: {error!r}") raise primary
def __enter__(self) -> Self: return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> Literal[False]: try: self.close() except BaseException as cleanup_error: if exc_value is None: raise exc_value.add_note(f"Additional site resource cleanup failure: {cleanup_error!r}") return False