SWMM_MCP_Server_Claude / sessions.py
razaali10's picture
Upload 20 files
e545bf5 verified
Raw
History Blame Contribute Delete
3.32 kB
"""Session store for the SWMM MCP server.
Each uploaded model gets a session with its own working directory under
/tmp/swmm_sessions. Sessions expire after SESSION_TTL_HOURS (default 6) and
are swept lazily on access. State is process-local: the HF Space runs a
single uvicorn worker, matching the stateful-session conclusion from the
WNTR MCP architecture work (one backend, session-ID state).
"""
from __future__ import annotations
import os
import shutil
import threading
import time
import uuid
from pathlib import Path
from typing import Any
SESSION_ROOT = Path(os.environ.get("SWMM_SESSION_ROOT", "/tmp/swmm_sessions"))
SESSION_TTL_S = float(os.environ.get("SESSION_TTL_HOURS", "6")) * 3600.0
MAX_SESSIONS = int(os.environ.get("MAX_SESSIONS", "40"))
class Session:
def __init__(self, session_id: str, workdir: Path):
self.id = session_id
self.workdir = workdir
self.created = time.time()
self.touched = time.time()
self.data: dict[str, Any] = {}
def touch(self) -> None:
self.touched = time.time()
class SessionStore:
def __init__(self) -> None:
self._sessions: dict[str, Session] = {}
self._lock = threading.Lock()
SESSION_ROOT.mkdir(parents=True, exist_ok=True)
def create(self) -> Session:
with self._lock:
self._sweep_locked()
if len(self._sessions) >= MAX_SESSIONS:
oldest = min(self._sessions.values(), key=lambda s: s.touched)
self._drop_locked(oldest.id)
sid = uuid.uuid4().hex[:12]
workdir = SESSION_ROOT / sid
workdir.mkdir(parents=True, exist_ok=True)
session = Session(sid, workdir)
self._sessions[sid] = session
return session
def get(self, session_id: str) -> Session:
with self._lock:
self._sweep_locked()
session = self._sessions.get(str(session_id))
if session is None:
raise KeyError(
f"Unknown or expired session '{session_id}'. Call upload_model first "
f"(sessions expire after {SESSION_TTL_S/3600:.0f} h of inactivity)."
)
session.touch()
return session
def list(self) -> list[dict[str, Any]]:
with self._lock:
self._sweep_locked()
return [
{
"session_id": s.id,
"model": s.data.get("filename"),
"simulated": bool(s.data.get("results")),
"age_minutes": round((time.time() - s.created) / 60.0, 1),
}
for s in self._sessions.values()
]
def drop(self, session_id: str) -> bool:
with self._lock:
return self._drop_locked(str(session_id))
# -- internal --
def _drop_locked(self, session_id: str) -> bool:
session = self._sessions.pop(session_id, None)
if session is None:
return False
shutil.rmtree(session.workdir, ignore_errors=True)
return True
def _sweep_locked(self) -> None:
now = time.time()
for sid in [s for s, v in self._sessions.items() if now - v.touched > SESSION_TTL_S]:
self._drop_locked(sid)
STORE = SessionStore()