Skip to content

Commit f395de1

Browse files
committed
updated dependencies etc
1 parent e27e43f commit f395de1

8 files changed

Lines changed: 602 additions & 540 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
66
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7-
[![Tests](https://img.shields.io/badge/tests-202%20passing-brightgreen.svg)]()
7+
[![Tests](https://img.shields.io/badge/tests-264%20passing-brightgreen.svg)]()
88
[![Coverage](https://img.shields.io/badge/coverage-90%25-brightgreen.svg)]()
99

1010
Dead simple session management with automatic expiration, multiple storage backends, and multi-tenant isolation. Perfect for web apps, APIs, and any system needing reliable sessions.
@@ -117,14 +117,15 @@ Session Lifecycle:
117117
[Done]
118118
```
119119

120-
## ✨ What's New in v0.5
120+
## ✨ What's New in v0.6
121121

122122
- **🎯 Pydantic Native**: All models are Pydantic-based with automatic validation
123123
- **🔒 Type-Safe Enums**: No more magic strings - `SessionStatus.ACTIVE`, `ProviderType.REDIS`
124124
- **📦 Exported Types**: Full IDE autocomplete for `SessionMetadata`, `CSRFTokenInfo`, etc.
125125
- **⚡ Async Native**: Built from ground-up for async/await
126126
- **🔄 Backward Compatible**: Existing code works unchanged
127-
- **✅ 90%+ Test Coverage**: 202 tests, battle-tested
127+
- **🗂️ Bounded LRU Cache**: In-process session cache is capped at 1024 entries (evicts LRU) — no unbounded memory growth
128+
- **✅ 90%+ Test Coverage**: 264 tests, battle-tested
128129

129130
```python
130131
from chuk_sessions import SessionManager, SessionStatus, SessionMetadata

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "chuk-sessions"
7-
version = "0.6.1"
7+
version = "0.6.2"
88
description = "CHUK Sessions provides a comprehensive, async-first session management system with automatic expiration, and support for both in-memory and Redis storage backends. Perfect for web applications, MCP servers, API gateways, and microservices that need reliable, scalable session handling."
99
readme = "README.md"
1010
license = {text = "Apache-2.0"}

src/chuk_sessions/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from __future__ import annotations
1111

1212
# Version
13-
__version__ = "0.5.1"
13+
__version__ = "0.6.2"
1414

1515
# Core imports
1616
from .api import get_session, session

src/chuk_sessions/models.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from pydantic import BaseModel, Field, model_validator
1616

1717
from .enums import SessionStatus, TokenType
18+
from .utils import utc_now_iso
1819

1920

2021
class SessionMetadata(BaseModel):
@@ -50,7 +51,7 @@ class SessionMetadata(BaseModel):
5051
@model_validator(mode="after")
5152
def set_default_timestamps(self) -> SessionMetadata:
5253
"""Set default timestamps if not provided."""
53-
now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
54+
now_iso = utc_now_iso()
5455
if self.created_at is None:
5556
self.created_at = now_iso
5657
if self.last_accessed is None:
@@ -66,9 +67,7 @@ def is_expired(self) -> bool:
6667

6768
def touch(self) -> None:
6869
"""Refresh the last-accessed timestamp to 'now'."""
69-
self.last_accessed = (
70-
datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
71-
)
70+
self.last_accessed = utc_now_iso()
7271

7372
# Backward compatibility methods
7473
def to_dict(self) -> dict[str, Any]:

src/chuk_sessions/providers/memory.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55

66
from __future__ import annotations
77

8+
import logging
89
import os
910
import time
1011
import asyncio
1112
from contextlib import asynccontextmanager
1213
from typing import Dict, Tuple, Any, Callable, AsyncContextManager
1314

15+
logger = logging.getLogger(__name__)
16+
1417
# Default TTL from environment or 1 hour
1518
_DEFAULT_TTL = int(os.getenv("SESSION_DEFAULT_TTL", "3600"))
1619

@@ -37,6 +40,7 @@ async def get(self, key: str):
3740
value, exp = entry
3841
if exp < time.time():
3942
del _MemorySession._cache[key]
43+
logger.debug("Memory session key expired and evicted: %s", key)
4044
return None
4145
return value
4246

src/chuk_sessions/session_manager.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import os
1919
import time
2020
import uuid
21+
from collections import OrderedDict
2122
from datetime import datetime, timedelta, timezone
2223
from typing import Any, AsyncContextManager, Callable, Optional
2324

@@ -75,8 +76,9 @@ def __init__(
7576

7677
self.session_factory = factory_for_env()
7778

78-
# local LRU-ish cache
79-
self._session_cache: dict[str, SessionMetadata] = {}
79+
# bounded LRU in-process cache
80+
self._max_cache_size = 1024
81+
self._session_cache: OrderedDict[str, SessionMetadata] = OrderedDict()
8082
self._cache_lock = asyncio.Lock()
8183

8284
logger.debug("SessionManager initialised for sandbox: %s", self.sandbox_id)
@@ -125,7 +127,7 @@ async def allocate_session(
125127
await self._store_session_metadata(metadata)
126128

127129
async with self._cache_lock:
128-
self._session_cache[session_id] = metadata
130+
self._cache_put(session_id, metadata)
129131

130132
logger.debug("Session allocated: %s (user=%s)", session_id, user_id)
131133
return session_id
@@ -140,7 +142,7 @@ async def validate_session(self, session_id: str) -> bool:
140142
return True
141143
return False
142144
except Exception as err:
143-
logger.debug("Session validation failed for %s: %s", session_id, err)
145+
logger.warning("Session validation failed for %s: %s", session_id, err)
144146
return False
145147

146148
async def get_session_info(self, session_id: str) -> Optional[dict[str, Any]]:
@@ -182,7 +184,7 @@ async def update_session_metadata(
182184
await self._store_session_metadata(metadata)
183185

184186
async with self._cache_lock:
185-
self._session_cache[session_id] = metadata
187+
self._cache_put(session_id, metadata)
186188
return True
187189
except Exception as err:
188190
logger.error(
@@ -207,7 +209,7 @@ async def extend_session_ttl(self, session_id: str, additional_hours: int) -> bo
207209
await self._store_session_metadata(metadata)
208210

209211
async with self._cache_lock:
210-
self._session_cache[session_id] = metadata
212+
self._cache_put(session_id, metadata)
211213
logger.debug("Extended session %s by %dh", session_id, additional_hours)
212214
return True
213215
except Exception as err:
@@ -235,6 +237,16 @@ async def delete_session(self, session_id: str) -> bool:
235237
# Internal helpers
236238
# ──────────────────────────────────────────────────────────────────────
237239

240+
def _cache_put(self, session_id: str, metadata: SessionMetadata) -> None:
241+
"""Insert/update an entry and evict the LRU entry if over capacity.
242+
243+
Must be called while holding _cache_lock.
244+
"""
245+
self._session_cache[session_id] = metadata
246+
self._session_cache.move_to_end(session_id)
247+
if len(self._session_cache) > self._max_cache_size:
248+
self._session_cache.popitem(last=False)
249+
238250
def _generate_session_id(self, user_id: Optional[str] = None) -> str:
239251
timestamp = int(time.time())
240252
rnd = uuid.uuid4().hex[:8]
@@ -266,7 +278,7 @@ async def _get_session_metadata(self, session_id: str) -> Optional[SessionMetada
266278
return None
267279
# re-cache
268280
async with self._cache_lock:
269-
self._session_cache[session_id] = metadata
281+
self._cache_put(session_id, metadata)
270282
return metadata
271283
except Exception as err:
272284
logger.debug("Failed fetching session %s: %s", session_id, err)
@@ -296,7 +308,7 @@ async def _store_session_metadata(self, metadata: SessionMetadata) -> None:
296308
await session.setex(key, ttl, json.dumps(metadata.to_dict()))
297309

298310
async with self._cache_lock:
299-
self._session_cache[metadata.session_id] = metadata
311+
self._cache_put(metadata.session_id, metadata)
300312
except Exception as err:
301313
logger.error("Session storage failed for %s: %s", metadata.session_id, err)
302314
raise SessionError(f"Session storage failed: {err}") from err
@@ -320,6 +332,7 @@ async def cleanup_expired_sessions(self) -> int:
320332
def get_cache_stats(self) -> dict[str, Any]:
321333
return {
322334
"cached_sessions": len(self._session_cache),
335+
"max_cache_size": self._max_cache_size,
323336
"sandbox_id": self.sandbox_id,
324337
"default_ttl_hours": self.default_ttl_hours,
325338
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from datetime import datetime, timezone
2+
3+
4+
def utc_now_iso() -> str:
5+
"""Return the current UTC time as an ISO 8601 string with a Z suffix."""
6+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
7+
8+
9+
__all__ = ["utc_now_iso"]

0 commit comments

Comments
 (0)