# Copyright 2025 Softwell S.r.l.
# Licensed under the Apache License, Version 2.0
"""Server-managed session with metadata, Bag data, and auth context.
A Session groups request-scoped state under a unique ID with expiry tracking.
SessionMiddleware creates or reconnects sessions via cookies and attaches
them to ``scope["session"]``. Each session holds an Avatar for the
authenticated user and a Bag for arbitrary application data.
"""
from __future__ import annotations
import time
from typing import Any
from genro_bag import Bag
from .avatar import Avatar
__all__ = ["Session"]
[docs]
class Session:
"""Server-managed session with meta, data (Bag), and auth snapshot.
Attributes:
_id: Unique session token.
_meta: Server-managed metadata (created_at, last_access, ttl).
_data: Application data as Bag.
_auth: Auth snapshot from session creation.
"""
__slots__ = ("_id", "_meta", "_data", "_auth", "_avatar")
[docs]
def __init__(self, session_id: str, auth: dict[str, Any] | None, ttl: int) -> None:
"""Initialize session.
Args:
session_id: Unique session token.
auth: Auth dict snapshot from authentication, or None.
ttl: Time-to-live in seconds.
"""
now = time.time()
self._id = session_id
self._meta: dict[str, Any] = {
"created_at": now,
"last_access": now,
"ttl": ttl,
}
self._data = Bag()
self._auth = auth
self._avatar = Avatar(auth=auth)
@property
def id(self) -> str:
"""Unique session token."""
return self._id
@property
def meta(self) -> dict[str, Any]:
"""Server-managed metadata: created_at, last_access, ttl."""
return self._meta
@property
def data(self) -> Bag:
"""Application data as Bag."""
return self._data
@property
def auth(self) -> dict[str, Any] | None:
"""Auth snapshot from session creation."""
return self._auth
@property
def avatar(self) -> Avatar:
"""User identity avatar built from auth snapshot."""
return self._avatar
[docs]
def touch(self) -> None:
"""Update last_access timestamp."""
self._meta["last_access"] = time.time()
[docs]
def is_expired(self) -> bool:
"""Check if session has exceeded its TTL."""
ttl: int = self._meta["ttl"]
if ttl <= 0:
return True
last_access: float = self._meta["last_access"]
return bool((time.time() - last_access) > ttl)
if __name__ == "__main__":
pass