Files
kaya/packages/kaya-session/src/kaya/session/_store.py
T

86 lines
2.9 KiB
Python

from abc import ABC, abstractmethod
from secrets import token_urlsafe
from time import monotonic
from typing import Any, Callable, Optional
from ._session import Session
class SessionStore(ABC):
"""Pluggable backend for session persistence."""
@abstractmethod
async def load(self, session_id: str, max_age: Optional[int] = None) -> Optional[Session]:
"""Load an existing session, or return ``None`` if unknown or expired.
``max_age`` is the idle timeout in seconds. If provided, the store may
use it to enforce a server-side expiry and to slide the expiry window
on each access.
"""
pass
@abstractmethod
async def save(self, session_id: str, session: Session, max_age: Optional[int] = None) -> None:
"""Persist the session data under ``session_id``.
``max_age`` is the idle timeout in seconds. If provided, the store
should record the expiry time as ``now + max_age``.
"""
pass
@abstractmethod
async def delete(self, session_id: str) -> None:
"""Remove the session from the store."""
pass
def new_session_id(self) -> str:
"""Return a new opaque session identifier.
Subclasses may override this to use a backend-specific ID generator.
"""
return token_urlsafe(32)
class InMemorySessionStore(SessionStore):
"""Simple in-memory session store.
Suitable for development and single-process deployments. Session data is
lost when the process exits and is not shared between processes.
Sessions can optionally expire server-side after ``max_age`` seconds of
inactivity. Active sessions slide the expiry window on each access.
"""
def __init__(self, clock: Callable[[], float] = monotonic) -> None:
self._clock = clock
self._data: dict[str, dict[str, Any]] = {}
self._expires: dict[str, float] = {}
async def load(self, session_id: str, max_age: Optional[int] = None) -> Optional[Session]:
data = self._data.get(session_id)
if data is None:
return None
expires = self._expires.get(session_id)
now = self._clock()
if expires is not None and now > expires:
self._data.pop(session_id, None)
self._expires.pop(session_id, None)
return None
if max_age is not None and expires is not None:
self._expires[session_id] = now + max_age
return Session(session_id, data)
async def save(self, session_id: str, session: Session, max_age: Optional[int] = None) -> None:
self._data[session_id] = dict(session)
if max_age is not None:
self._expires[session_id] = self._clock() + max_age
else:
self._expires.pop(session_id, None)
async def delete(self, session_id: str) -> None:
self._data.pop(session_id, None)
self._expires.pop(session_id, None)