Sessions

The session mixin, the session object, the avatar, and the session store.

Session capability: HTTP sessions as a mixin over the base server (D16).

SessionMixin is composed BEFORE MiddlewareMixin and BaseServer (class S(SessionMixin, MiddlewareMixin, BaseServer)). Its cooperative __init__ peels session_store= (None → a fresh MemorySessionStore) and session_ttl= (the default store’s TTL), then ARMS SessionMiddleware by injecting {"session": True} into the middleware config it forwards to MiddlewareMixin along the cooperative chain — composing the two mixins arms sessions with no user action, while an explicit middleware={"session": False} still wins (setdefault never overrides an explicit switch). It overrides the §4 contract method session(request) to return the session attached to the request scope; a composition WITHOUT the mixin keeps the base answer (None). The login seam is not a server method: a handler attaches the identity through the request facade (request.session.attach_avatar(avatar)) — the session id never changes at login, so the cookie already held by the client stays valid.

save_session= arms the pickle snapshot, the development survival line the CLI wires from serve --name (~/.genroasgi/sessions/<name>.pickle): __call__ intercepts the lifespan scope exactly like TaskMixinlifespan.py is NEVER touched (ratified) — loading the snapshot before the protocol runs (an absent file starts empty) and saving EVERY live session, data Bag included, when the protocol completes at shutdown. Unarmed, every scope passes straight through.

class genro_asgi.session.mixin.SessionMixin(**kwargs)[source]

Bases: object

Session capability mixin, composed BEFORE the middleware/server classes.

Constructor kwargs peeled here: session_store — an explicit store (None builds a MemorySessionStore); session_ttl — the default store’s TTL when no explicit store is given; save_session — the snapshot pickle path (None, the default, disarms the snapshot).

Parameters:

kwargs (Any)

property session_store: SessionStore

The store backing this server’s sessions.

property save_session: Path | None

The snapshot pickle path, or None when the snapshot is disarmed.

session(request)[source]

The session attached to the request scope, or None if none.

Return type:

Any

Parameters:

request (Any)

Server-managed session: id, meta, Bag data, and a keyed collection of Avatars.

A Session groups request-scoped state under a unique id with expiry tracking. SessionMiddleware creates or reconnects sessions via the request cookie and attaches them to scope["session"].

The dressing model. A session is not one identity but a wardrobe of them, each stored under a key. The ROOT_AVATAR_KEY slot holds the identity of the primary login — the one the auth chain resolves and the one avatar() returns with no argument. Further keys are sub-logins: an identity a page acquired inside the same session (a second-system credential, an impersonation, a delegated account) that must coexist with the root one instead of replacing it. Page trees will reference the slot they are dressed in by avatar_key, so the identity of a page is a lookup in this collection, never a copy of it.

avatar(key) returns Avatar | NoneNone is an unclaimed slot, and an absent root slot is an anonymous session; capturing an identity is an explicit avatar= at creation (the root slot) or an attach_avatar call. avatars is a read-only view for enumeration; attach_avatar is its only writer. There is no detach: a slot claimed in a session stays claimed for its lifetime. data is a Bag for arbitrary application data. touch() refreshes last_access; is_expired() measures the TTL from it.

Write-back is explicit (D24): a session persists at request end ONLY when dirty is set. attach_avatar marks it dirty (a login must survive), and a handler mutating data marks it dirty with mark_dirty() — there is no write-through. touch() is NOT a mutation for this purpose: the last_access refresh happens on every get (including read-only requests), so making it dirty would save on every request and defeat the zero-I/O read path. The middleware clears the flag with clear_dirty() after a successful save.

class genro_asgi.session.session.Session(session_id, avatar, ttl)[source]

Bases: object

Server-managed session with meta, Bag data, and keyed identity avatars.

Parameters:
__init__(session_id, avatar, ttl)[source]

Initialize the session with its token, its root avatar, and a TTL.

Parameters:
Return type:

None

property id: str

Unique session token.

property meta: dict[str, Any]

created_at, last_access, ttl.

Type:

Server-managed metadata

property data: Bag

Application data as a Bag.

avatar(key='root')[source]

The avatar dressed under key; None = unclaimed slot.

With no argument it returns the root avatar — the primary login — so an anonymous session answers None.

Return type:

Avatar | None

Parameters:

key (str)

property avatars: Mapping[str, Avatar]

Read-only view of the keyed avatars (attach_avatar is the only writer).

property dirty: bool

Whether the session has unsaved changes to persist at request end.

attach_avatar(avatar, key='root')[source]

Dress key with an avatar — the login event (marks the session dirty).

The session stays the same object: id, data and meta are untouched, so whatever an anonymous visitor accumulated survives the login. The default key is the root slot (the primary login); any other key is a sub-login coexisting with it. The change is marked dirty so the login persists.

Return type:

None

Parameters:
mark_dirty()[source]

Flag the session as changed — a handler mutating data calls this.

Return type:

None

clear_dirty()[source]

Reset the dirty flag (the middleware calls this after a successful save).

Return type:

None

touch()[source]

Refresh last_access to now (NOT a dirty-making change; see module doc).

Return type:

None

is_expired()[source]

Whether the session has exceeded its TTL (non-positive TTL = expired).

Return type:

bool

Avatar — the ONE authenticated identity type across the package.

An Avatar is a plain slotted value object: an identity string, its authorization tags, and an extensible Bag of per-user data. AuthCore returns it and sessions carry it — “nobody” is None uniformly, never an anonymous Avatar. The constructor normalizes tags=None to an empty list (the identity boundary for e.g. a JWT null tags claim). No framework machinery.

class genro_asgi.session.avatar.Avatar(identity, tags=None)[source]

Bases: object

User identity with authorization tags and extensible Bag data.

Parameters:
__init__(identity, tags=None)[source]

Build the avatar; tags=None normalizes to an empty list.

Parameters:
Return type:

None

property identity: str

User identifier (username, email, …).

property tags: list[str]

Authorization tags (roles/permissions).

property data: Bag

Extensible per-user data as a Bag.

Session store — the storage Protocol and the in-memory default.

SessionStore is a runtime-checkable Protocol (get/create/delete/ purge_expired/dump/restore). Its test suite is a shared CONTRACT suite driven by a store factory (§5.9), so a custom backend plugs into the SAME tests. MemorySessionStore is the dict-backed only shipped store: secrets tokens, a default_ttl for new sessions, lazy expiry on get, and a delta-checked purge_expired at create time — the mass reap runs only when PURGE_INTERVAL has elapsed since the last one (no background task: this REPLACES the former TaskManager purge loop, a ratified revision of core 1e/◆D22). dump/restore persist meta and the keyed avatars’ identity/tags only — never the data Bag. The serialized shape is avatars: {key: {identity, tags}}, the whole wardrobe of the session. save_snapshot/load_snapshot are the OTHER persistence pair — one pickle file carrying every live session whole, data Bag included, the development survival line SessionMixin drives around the lifespan. create() is anonymous by default (avatar is None); capturing an identity into a session is an explicit create(avatar=...), which dresses the root slot.

class genro_asgi.session.store.SessionStore(*args, **kwargs)[source]

Bases: Protocol

Protocol for session storage backends.

get(session_id)[source]

Retrieve a session by id, or None if unknown or expired.

Return type:

Session | None

Parameters:

session_id (str)

create(avatar=None)[source]

Create a new session with a unique token (anonymous by default).

avatar dresses the session’s root slot.

Return type:

Session

Parameters:

avatar (Avatar | None)

save(session)[source]

Persist a dirty session’s state (the middleware calls this at request end).

Return type:

None

Parameters:

session (Session)

delete(session_id)[source]

Remove a session from the store.

Return type:

None

Parameters:

session_id (str)

purge_expired()[source]

Remove every expired session; return how many were purged.

Return type:

int

dump()[source]

Serialize the sessions for persistence.

Return type:

dict[str, Any]

restore(data)[source]

Restore sessions from serialized data.

Return type:

None

Parameters:

data (dict[str, Any])

class genro_asgi.session.store.MemorySessionStore(default_ttl=3600)[source]

Bases: object

In-memory session store — the default implementation.

Parameters:

default_ttl (int)

__init__(default_ttl=3600)[source]

Initialize an empty store with a default TTL for new sessions.

Parameters:

default_ttl (int)

Return type:

None

get(session_id)[source]

Retrieve a session by id; drop and return None if it has expired.

Return type:

Session | None

Parameters:

session_id (str)

create(avatar=None)[source]

Create a session (default TTL); a delta-checked mass reap runs first.

The reap is opportunistic AND throttled: it runs only when PURGE_INTERVAL has elapsed since the last one, so a burst of creates never pays a full-store scan each time.

Return type:

Session

Parameters:

avatar (Avatar | None)

save(session)[source]

No-op: the in-memory store holds the live object, so it is already saved.

Return type:

None

Parameters:

session (Session)

delete(session_id)[source]

Remove a session from the store (a no-op if absent).

Return type:

None

Parameters:

session_id (str)

purge_expired()[source]

Drop every expired session from the store; return the count purged.

Return type:

int

dump()[source]

Serialize meta and every keyed avatar’s identity/tags (never the data Bag).

Return type:

dict[str, Any]

save_snapshot(path)[source]

Pickle EVERY live session — data Bag INCLUDED — to path.

The development survival line (genro-asgi serve --name): the whole store crosses a restart through one pickle file. This deliberately supersedes the dump/restore contract (“the data Bag is never persisted”) for the snapshot path. Expired sessions are reaped first; parent directories are created. Returns how many sessions were saved.

Return type:

int

Parameters:

path (str | Path)

load_snapshot(path)[source]

Repopulate the store from a save_snapshot file, dropping expired ones.

The TTL is the only filter: a session whose last_access is still within its ttl comes back whole (data Bag included). Returns how many sessions were restored.

Return type:

int

Parameters:

path (str | Path)

restore(data)[source]

Restore non-expired sessions from dump() output (meta + rebuilt avatars).

Return type:

None

Parameters:

data (dict[str, Any])