Sessions

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

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.

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.

Parameters:

kwargs (Any)

property session_store: SessionStore

The store backing this server’s sessions.

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 an optional Avatar.

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"]. Each session carries an Avatar | NoneNone is an anonymous session; capturing an identity is an explicit avatar= at creation — and 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 an optional Avatar.

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

Initialize the session with its token, an identity 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.

property avatar: Avatar | None

Identity avatar; None = anonymous session.

property dirty: bool

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

attach_avatar(avatar)[source]

Attach the identity 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 change is marked dirty so the login persists.

Return type:

None

Parameters:

avatar (Avatar)

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 parametrized over implementations (§5.9), so the core 1b file/db backends plug into the SAME tests. MemorySessionStore is the dict-backed default: secrets tokens, a default_ttl for new sessions, lazy expiry on get, opportunistic purge_expired at create time (no background task — those arrive in core 1e), and a dump/restore that persists meta and the avatar’s identity/tags only — never the data Bag. create() is anonymous by default (avatar is None); capturing an identity into a session is an explicit create(avatar=...).

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).

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), purging expired ones opportunistically first.

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 the avatar’s identity/tags per session (never the data Bag).

Return type:

dict[str, Any]

restore(data)[source]

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

Return type:

None

Parameters:

data (dict[str, Any])

FileSessionStore — one JSON file per session over a storage mount.

Like MemorySessionStore this store keeps live Session objects in memory (so get returns the same instance and touch is honored) but mirrors each to <mount>:<prefix>/<id>.json, so a session survives a process restart or a fresh store built on the SAME mount — the D22 survival line. The file carries meta (created_at/last_access/ttl) and the avatar’s identity/tags ONLY, keyed on disk by the session id (the filename); the data Bag is VOLATILE — never persisted — the SAME contract as dump/restore (full-data persistence is a future decision). All I/O is synchronous through the Phase 1 storage nodes (core 1b ratified: async callers wrap in server.run_sync()).

purge_expired reaps the sessions this process tracks in memory (removing their files too) and is invoked opportunistically at create time — no background task, those arrive in core 1e. An expired file the store has not loaded is reaped lazily the next time get touches it (no plain-text fallback, no silent skip on a corrupt file: a non-JSON payload raises).

class genro_asgi.session.file_store.FileSessionStore(storage, mount='site', prefix='sessions', default_ttl=3600)[source]

Bases: object

Session store persisting one JSON file per session on a storage mount.

Parameters:
__init__(storage, mount='site', prefix='sessions', default_ttl=3600)[source]

Bind the store to a storage mount/prefix with a default TTL.

Parameters:
Return type:

None

get(session_id)[source]

Retrieve a session (cache first, then disk); expired ones are reaped.

Return type:

Session | None

Parameters:

session_id (str)

create(avatar=None)[source]

Create and persist a session, purging expired ones opportunistically first.

Return type:

Session

Parameters:

avatar (Avatar | None)

save(session)[source]

Persist a session’s current state to disk (the write-back seam).

Return type:

None

Parameters:

session (Session)

delete(session_id)[source]

Remove a session from the cache and disk (a no-op if absent).

Return type:

None

Parameters:

session_id (str)

purge_expired()[source]

Drop every expired tracked session (and its file); return the count purged.

Return type:

int

dump()[source]

Serialize the tracked sessions (meta + avatar per session; never the data Bag).

Return type:

dict[str, Any]

restore(data)[source]

Restore non-expired sessions from dump() output (cached and persisted).

Return type:

None

Parameters:

data (dict[str, Any])