Source code for picobox.ext.asgiscopes

"""Scopes for ASGI applications."""

import contextvars
import typing as t
import weakref

import picobox

_current_app_store = contextvars.ContextVar(f"{__name__}.current-app-store")
_current_req_store = contextvars.ContextVar(f"{__name__}.current-req-store")


[docs] class ScopeMiddleware: """A ASGI middleware that defines scopes for Picobox. For the proper functioning of :class:`application` and :class:`request` scopes, it is essential to integrate this middleware into your ASGI application. Otherwise, the aforementioned scopes will be inoperable. .. code:: python from picobox.ext import asgiscopes app = asgiscopes.ScopeMiddleware(app) :param app: The ASGI application to wrap. """ def __init__(self, app): self.app = app # Since we want stored objects to be garbage collected as soon as the # storing scope instance is destroyed, scope instances have to be # weakly referenced. self.store = weakref.WeakKeyDictionary() async def __call__(self, scope, receive, send): """Define scopes and invoke the ASGI application.""" # Storing the ASGI application's scope state within a ScopeMiddleware # instance because it's assumed that each ASGI middleware is typically # applied once to a given ASGI application. By keeping the application # scope state in the middleware, we facilitate support for multiple # simultaneous ASGI applications (e.g., in nested execution scenarios). app_store_token = _current_app_store.set(self.store) req_store_token = _current_req_store.set(weakref.WeakKeyDictionary()) try: await self.app(scope, receive, send) finally: _current_req_store.reset(req_store_token) _current_app_store.reset(app_store_token)
class _asgiscope(picobox.Scope): """A base class for ASGI scopes.""" _store_cvar: contextvars.ContextVar @property def _store(self) -> t.MutableMapping[t.Hashable, t.Any]: try: store = self._store_cvar.get() except LookupError: raise RuntimeError( "Working outside of ASGI context.\n" "\n" "This typically means that you attempted to use picobox with " "ASGI scopes, but 'picobox.ext.asgiscopes.ScopeMiddleware' has " "not been used with your ASGI application." ) try: store = store[self] except KeyError: store = store.setdefault(self, {}) return store def set(self, key: t.Hashable, value: t.Any) -> None: self._store[key] = value def get(self, key: t.Hashable) -> t.Any: return self._store[key]
[docs] class application(_asgiscope): """Share instances across the same ASGI application. In typical scenarios, a single ASGI application exists, making this scope interchangeable with :class:`picobox.singleton`. However, unlike the latter, the application scope ensures that dependencies are bound to the lifespan of a specific application instance. This is particularly useful in testing scenarios where each test involves creating a new application instance or in situations where applications are nested. Requires :class:`ScopeMiddleware`; otherwise ``RuntimeError`` is thrown. .. versionadded:: 4.1 """ _store_cvar = _current_app_store
[docs] class request(_asgiscope): """Share instances across the same ASGI (HTTP/WebSocket) request. You might want to store your SQLAlchemy session or Request-ID per request. In many cases this produces much more readable code than passing the whole request context around. Requires :class:`ScopeMiddleware`; otherwise ``RuntimeError`` is thrown. .. versionadded:: 4.1 """ _store_cvar = _current_req_store