Source code for genro_asgi.session.mixin

# Copyright 2025 Softwell S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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

from __future__ import annotations

from typing import Any

from .store import MemorySessionStore, SessionStore

__all__ = ["SessionMixin"]


[docs] class SessionMixin: """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. """ def __init__(self, **kwargs: Any) -> None: store: SessionStore | None = kwargs.pop("session_store", None) ttl: int | None = kwargs.pop("session_ttl", None) middleware: dict[str, Any] = dict(kwargs.get("middleware") or {}) middleware.setdefault("session", True) kwargs["middleware"] = middleware super().__init__(**kwargs) if store is None: store = MemorySessionStore() if ttl is None else MemorySessionStore(default_ttl=ttl) self._session_store = store @property def session_store(self) -> SessionStore: """The store backing this server's sessions.""" return self._session_store
[docs] def session(self, request: Any) -> Any: """The session attached to the request scope, or ``None`` if none.""" return request.get("session") if request is not None else None
if __name__ == "__main__": from ..application import BaseApplication from ..middleware import MiddlewareMixin from ..server import BaseServer from .avatar import Avatar class DemoServer(SessionMixin, MiddlewareMixin, BaseServer): pass server = DemoServer(primary=BaseApplication()) assert isinstance(server.session_store, SessionStore) assert server.session({"session": "S"}) == "S" assert server.session({}) is None assert BaseServer(primary=BaseApplication()).session({}) is None anonymous = server.session_store.create() anonymous.data["cart"] = "kept" # login seam via the request facade: attach the avatar to the session in place anonymous.attach_avatar(Avatar("alice", ["admin"])) assert anonymous.avatar is not None and anonymous.avatar.identity == "alice" assert anonymous.data["cart"] == "kept" # same session, same id — the cart survives