proxilion 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,735 @@
1
+ """
2
+ Session tracking for AI agent conversations.
3
+
4
+ Provides session management with metadata, expiration, and
5
+ multi-turn conversation state.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import threading
11
+ import uuid
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime, timezone
14
+ from enum import Enum
15
+ from typing import Any
16
+
17
+ from proxilion.context.message_history import Message, MessageHistory, MessageRole
18
+ from proxilion.types import UserContext
19
+
20
+
21
+ class SessionState(Enum):
22
+ """State of a session."""
23
+
24
+ ACTIVE = "active"
25
+ IDLE = "idle"
26
+ EXPIRED = "expired"
27
+ TERMINATED = "terminated"
28
+
29
+
30
+ @dataclass
31
+ class SessionConfig:
32
+ """
33
+ Configuration for session management.
34
+
35
+ Attributes:
36
+ max_duration: Maximum session duration in seconds. None for no limit.
37
+ max_idle_time: Maximum idle time before expiration in seconds.
38
+ max_messages: Maximum messages per session.
39
+ max_tokens: Maximum total tokens per session.
40
+ auto_cleanup: Whether to automatically cleanup expired sessions.
41
+ metadata_schema: Optional schema for validating session metadata.
42
+ """
43
+
44
+ max_duration: int | None = 3600 # 1 hour default
45
+ max_idle_time: int | None = 900 # 15 minutes default
46
+ max_messages: int | None = 100
47
+ max_tokens: int | None = None
48
+ auto_cleanup: bool = True
49
+ metadata_schema: dict[str, Any] | None = None
50
+
51
+ def to_dict(self) -> dict[str, Any]:
52
+ """Convert config to dictionary."""
53
+ return {
54
+ "max_duration": self.max_duration,
55
+ "max_idle_time": self.max_idle_time,
56
+ "max_messages": self.max_messages,
57
+ "max_tokens": self.max_tokens,
58
+ "auto_cleanup": self.auto_cleanup,
59
+ "metadata_schema": self.metadata_schema,
60
+ }
61
+
62
+ @classmethod
63
+ def from_dict(cls, data: dict[str, Any]) -> SessionConfig:
64
+ """Create config from dictionary."""
65
+ return cls(
66
+ max_duration=data.get("max_duration", 3600),
67
+ max_idle_time=data.get("max_idle_time", 900),
68
+ max_messages=data.get("max_messages", 100),
69
+ max_tokens=data.get("max_tokens"),
70
+ auto_cleanup=data.get("auto_cleanup", True),
71
+ metadata_schema=data.get("metadata_schema"),
72
+ )
73
+
74
+
75
+ @dataclass
76
+ class Session:
77
+ """
78
+ Individual session with state and message history.
79
+
80
+ Attributes:
81
+ session_id: Unique identifier for this session.
82
+ user: The user context for this session.
83
+ config: Session configuration.
84
+ created_at: When the session was created.
85
+ last_activity: Last activity timestamp.
86
+ state: Current session state.
87
+ metadata: Session metadata (e.g., agent info, preferences).
88
+ history: Message history for this session.
89
+ termination_reason: Reason for termination if terminated.
90
+ """
91
+
92
+ session_id: str
93
+ user: UserContext
94
+ config: SessionConfig
95
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
96
+ last_activity: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
97
+ state: SessionState = SessionState.ACTIVE
98
+ metadata: dict[str, Any] = field(default_factory=dict)
99
+ history: MessageHistory = field(default=None) # type: ignore
100
+ termination_reason: str | None = None
101
+ _lock: threading.RLock = field(default_factory=threading.RLock, repr=False)
102
+
103
+ def __post_init__(self) -> None:
104
+ """Initialize message history if not provided."""
105
+ if self.history is None:
106
+ self.history = MessageHistory(
107
+ max_messages=self.config.max_messages,
108
+ max_tokens=self.config.max_tokens,
109
+ )
110
+
111
+ def add_message(
112
+ self,
113
+ role: MessageRole,
114
+ content: str,
115
+ metadata: dict[str, Any] | None = None,
116
+ ) -> Message:
117
+ """
118
+ Add a message to the session history.
119
+
120
+ Args:
121
+ role: The role of the message sender.
122
+ content: The message content.
123
+ metadata: Optional metadata for the message.
124
+
125
+ Returns:
126
+ The created message.
127
+
128
+ Raises:
129
+ ValueError: If session is not active.
130
+ """
131
+ with self._lock:
132
+ if self.state not in (SessionState.ACTIVE, SessionState.IDLE):
133
+ raise ValueError(
134
+ f"Cannot add message to session in state {self.state.value}"
135
+ )
136
+
137
+ message = Message(
138
+ role=role,
139
+ content=content,
140
+ metadata=metadata or {},
141
+ )
142
+ self.history.append(message)
143
+ self.touch()
144
+ return message
145
+
146
+ def get_messages(self, limit: int | None = None) -> list[Message]:
147
+ """
148
+ Get messages from the session history.
149
+
150
+ Args:
151
+ limit: Maximum number of messages to return (most recent).
152
+
153
+ Returns:
154
+ List of messages.
155
+ """
156
+ with self._lock:
157
+ if limit is not None:
158
+ return self.history.get_recent(limit)
159
+ return self.history.get_messages()
160
+
161
+ def get_context_for_llm(
162
+ self,
163
+ max_tokens: int | None = None,
164
+ provider: str = "openai",
165
+ ) -> list[dict[str, Any]]:
166
+ """
167
+ Get message history formatted for LLM API calls.
168
+
169
+ Args:
170
+ max_tokens: Maximum tokens to include (truncates from start).
171
+ provider: The LLM provider format ("openai", "anthropic", "google").
172
+
173
+ Returns:
174
+ List of message dictionaries for the LLM API.
175
+ """
176
+ with self._lock:
177
+ if max_tokens is not None:
178
+ # Create a temporary history to apply truncation
179
+ temp_history = MessageHistory()
180
+ for msg in self.history.get_messages():
181
+ temp_history.append(
182
+ Message(
183
+ role=msg.role,
184
+ content=msg.content,
185
+ timestamp=msg.timestamp,
186
+ metadata=msg.metadata,
187
+ token_count=msg.token_count,
188
+ message_id=msg.message_id,
189
+ )
190
+ )
191
+ temp_history.truncate_to_token_limit(max_tokens)
192
+ return temp_history.to_llm_format(provider)
193
+
194
+ return self.history.to_llm_format(provider)
195
+
196
+ def set_metadata(self, key: str, value: Any) -> None:
197
+ """
198
+ Set a metadata value.
199
+
200
+ Args:
201
+ key: The metadata key.
202
+ value: The metadata value.
203
+ """
204
+ with self._lock:
205
+ self.metadata[key] = value
206
+
207
+ def get_metadata(self, key: str, default: Any = None) -> Any:
208
+ """
209
+ Get a metadata value.
210
+
211
+ Args:
212
+ key: The metadata key.
213
+ default: Default value if key not found.
214
+
215
+ Returns:
216
+ The metadata value or default.
217
+ """
218
+ with self._lock:
219
+ return self.metadata.get(key, default)
220
+
221
+ def touch(self) -> None:
222
+ """Update last activity time and set state to active."""
223
+ with self._lock:
224
+ self.last_activity = datetime.now(timezone.utc)
225
+ if self.state == SessionState.IDLE:
226
+ self.state = SessionState.ACTIVE
227
+
228
+ def is_expired(self) -> bool:
229
+ """
230
+ Check if the session has expired.
231
+
232
+ Returns:
233
+ True if session is expired, False otherwise.
234
+ """
235
+ with self._lock:
236
+ if self.state in (SessionState.EXPIRED, SessionState.TERMINATED):
237
+ return True
238
+
239
+ now = datetime.now(timezone.utc)
240
+
241
+ # Check max duration
242
+ if self.config.max_duration is not None:
243
+ duration = (now - self.created_at).total_seconds()
244
+ if duration > self.config.max_duration:
245
+ self.state = SessionState.EXPIRED
246
+ return True
247
+
248
+ # Check idle time
249
+ if self.config.max_idle_time is not None:
250
+ idle_time = (now - self.last_activity).total_seconds()
251
+ if idle_time > self.config.max_idle_time:
252
+ self.state = SessionState.EXPIRED
253
+ return True
254
+
255
+ return False
256
+
257
+ def check_idle(self) -> bool:
258
+ """
259
+ Check if session should be marked as idle.
260
+
261
+ Returns:
262
+ True if session is idle, False otherwise.
263
+ """
264
+ with self._lock:
265
+ if self.state != SessionState.ACTIVE:
266
+ return self.state == SessionState.IDLE
267
+
268
+ if self.config.max_idle_time is not None:
269
+ now = datetime.now(timezone.utc)
270
+ idle_time = (now - self.last_activity).total_seconds()
271
+ # Mark as idle after half the max idle time
272
+ if idle_time > self.config.max_idle_time / 2:
273
+ self.state = SessionState.IDLE
274
+ return True
275
+
276
+ return False
277
+
278
+ def terminate(self, reason: str | None = None) -> None:
279
+ """
280
+ Terminate the session.
281
+
282
+ Args:
283
+ reason: Optional reason for termination.
284
+ """
285
+ with self._lock:
286
+ self.state = SessionState.TERMINATED
287
+ self.termination_reason = reason
288
+
289
+ def get_duration(self) -> float:
290
+ """
291
+ Get session duration in seconds.
292
+
293
+ Returns:
294
+ Duration in seconds.
295
+ """
296
+ with self._lock:
297
+ now = datetime.now(timezone.utc)
298
+ return (now - self.created_at).total_seconds()
299
+
300
+ def get_idle_time(self) -> float:
301
+ """
302
+ Get time since last activity in seconds.
303
+
304
+ Returns:
305
+ Idle time in seconds.
306
+ """
307
+ with self._lock:
308
+ now = datetime.now(timezone.utc)
309
+ return (now - self.last_activity).total_seconds()
310
+
311
+ def get_remaining_duration(self) -> float | None:
312
+ """
313
+ Get remaining session duration in seconds.
314
+
315
+ Returns:
316
+ Remaining duration in seconds, or None if no limit.
317
+ """
318
+ with self._lock:
319
+ if self.config.max_duration is None:
320
+ return None
321
+ elapsed = self.get_duration()
322
+ return max(0, self.config.max_duration - elapsed)
323
+
324
+ def to_dict(self) -> dict[str, Any]:
325
+ """
326
+ Serialize session to dictionary.
327
+
328
+ Returns:
329
+ Dictionary representation of the session.
330
+ """
331
+ with self._lock:
332
+ return {
333
+ "session_id": self.session_id,
334
+ "user_id": self.user.user_id,
335
+ "user_roles": self.user.roles,
336
+ "config": self.config.to_dict(),
337
+ "created_at": self.created_at.isoformat(),
338
+ "last_activity": self.last_activity.isoformat(),
339
+ "state": self.state.value,
340
+ "metadata": self.metadata,
341
+ "history": self.history.to_dict(),
342
+ "termination_reason": self.termination_reason,
343
+ }
344
+
345
+ @classmethod
346
+ def from_dict(cls, data: dict[str, Any]) -> Session:
347
+ """
348
+ Deserialize session from dictionary.
349
+
350
+ Args:
351
+ data: Dictionary with session data.
352
+
353
+ Returns:
354
+ Session instance.
355
+ """
356
+ user = UserContext(
357
+ user_id=data["user_id"],
358
+ roles=data.get("user_roles", []),
359
+ )
360
+ config = SessionConfig.from_dict(data.get("config", {}))
361
+ history = MessageHistory.from_dict(data.get("history", {}))
362
+
363
+ session = cls(
364
+ session_id=data["session_id"],
365
+ user=user,
366
+ config=config,
367
+ created_at=datetime.fromisoformat(data["created_at"]),
368
+ last_activity=datetime.fromisoformat(data["last_activity"]),
369
+ state=SessionState(data.get("state", "active")),
370
+ metadata=data.get("metadata", {}),
371
+ history=history,
372
+ termination_reason=data.get("termination_reason"),
373
+ )
374
+ return session
375
+
376
+
377
+ class SessionManager:
378
+ """
379
+ Manages multiple sessions with lifecycle management.
380
+
381
+ Provides session creation, retrieval, and cleanup for
382
+ multi-user agent applications.
383
+
384
+ Attributes:
385
+ config: Default configuration for new sessions.
386
+ cleanup_interval: Interval in seconds for automatic cleanup.
387
+
388
+ Example:
389
+ >>> from proxilion.types import UserContext
390
+ >>> config = SessionConfig(max_duration=3600, max_messages=100)
391
+ >>> manager = SessionManager(config)
392
+ >>> user = UserContext(user_id="user_123", roles=["user"])
393
+ >>> session = manager.create_session(user)
394
+ >>> session.add_message(MessageRole.USER, "Hello!")
395
+ >>> manager.get_session(session.session_id)
396
+ <Session ...>
397
+ """
398
+
399
+ def __init__(
400
+ self,
401
+ config: SessionConfig | None = None,
402
+ cleanup_interval: int = 300,
403
+ ) -> None:
404
+ """
405
+ Initialize session manager.
406
+
407
+ Args:
408
+ config: Default configuration for new sessions.
409
+ cleanup_interval: Interval in seconds for automatic cleanup.
410
+ """
411
+ self.config = config or SessionConfig()
412
+ self.cleanup_interval = cleanup_interval
413
+ self._sessions: dict[str, Session] = {}
414
+ self._user_sessions: dict[str, list[str]] = {} # user_id -> session_ids
415
+ self._lock = threading.RLock()
416
+ self._last_cleanup = datetime.now(timezone.utc)
417
+
418
+ def create_session(
419
+ self,
420
+ user: UserContext,
421
+ session_id: str | None = None,
422
+ config: SessionConfig | None = None,
423
+ metadata: dict[str, Any] | None = None,
424
+ ) -> Session:
425
+ """
426
+ Create a new session for a user.
427
+
428
+ Args:
429
+ user: The user context.
430
+ session_id: Optional session ID. Auto-generated if not provided.
431
+ config: Optional session-specific configuration.
432
+ metadata: Optional initial metadata.
433
+
434
+ Returns:
435
+ The created session.
436
+ """
437
+ with self._lock:
438
+ if session_id is None:
439
+ session_id = str(uuid.uuid4())
440
+
441
+ # Use provided config or fall back to default
442
+ session_config = config or self.config
443
+
444
+ session = Session(
445
+ session_id=session_id,
446
+ user=user,
447
+ config=session_config,
448
+ metadata=metadata or {},
449
+ )
450
+
451
+ self._sessions[session_id] = session
452
+
453
+ # Track sessions by user
454
+ if user.user_id not in self._user_sessions:
455
+ self._user_sessions[user.user_id] = []
456
+ self._user_sessions[user.user_id].append(session_id)
457
+
458
+ # Run cleanup if needed
459
+ if self.config.auto_cleanup:
460
+ self._maybe_cleanup()
461
+
462
+ return session
463
+
464
+ def get_session(self, session_id: str) -> Session | None:
465
+ """
466
+ Get a session by ID.
467
+
468
+ Args:
469
+ session_id: The session ID.
470
+
471
+ Returns:
472
+ The session if found and not expired, None otherwise.
473
+ """
474
+ with self._lock:
475
+ session = self._sessions.get(session_id)
476
+ if session is None:
477
+ return None
478
+
479
+ # Check expiration
480
+ if session.is_expired():
481
+ return None
482
+
483
+ return session
484
+
485
+ def get_user_sessions(
486
+ self,
487
+ user_id: str,
488
+ include_expired: bool = False,
489
+ ) -> list[Session]:
490
+ """
491
+ Get all sessions for a user.
492
+
493
+ Args:
494
+ user_id: The user ID.
495
+ include_expired: Whether to include expired sessions.
496
+
497
+ Returns:
498
+ List of sessions for the user.
499
+ """
500
+ with self._lock:
501
+ session_ids = self._user_sessions.get(user_id, [])
502
+ sessions: list[Session] = []
503
+
504
+ for sid in session_ids:
505
+ session = self._sessions.get(sid)
506
+ if session is None:
507
+ continue
508
+ if not include_expired and session.is_expired():
509
+ continue
510
+ sessions.append(session)
511
+
512
+ return sessions
513
+
514
+ def get_or_create_session(
515
+ self,
516
+ user: UserContext,
517
+ session_id: str | None = None,
518
+ config: SessionConfig | None = None,
519
+ ) -> tuple[Session, bool]:
520
+ """
521
+ Get an existing session or create a new one.
522
+
523
+ Args:
524
+ user: The user context.
525
+ session_id: Optional session ID to look up.
526
+ config: Optional session configuration for creation.
527
+
528
+ Returns:
529
+ Tuple of (session, created) where created is True if new.
530
+ """
531
+ with self._lock:
532
+ if session_id is not None:
533
+ existing = self.get_session(session_id)
534
+ if existing is not None:
535
+ return existing, False
536
+
537
+ session = self.create_session(user, session_id, config)
538
+ return session, True
539
+
540
+ def terminate_session(
541
+ self,
542
+ session_id: str,
543
+ reason: str | None = None,
544
+ ) -> bool:
545
+ """
546
+ Terminate a session.
547
+
548
+ Args:
549
+ session_id: The session ID.
550
+ reason: Optional reason for termination.
551
+
552
+ Returns:
553
+ True if session was found and terminated.
554
+ """
555
+ with self._lock:
556
+ session = self._sessions.get(session_id)
557
+ if session is None:
558
+ return False
559
+
560
+ session.terminate(reason)
561
+ return True
562
+
563
+ def terminate_user_sessions(
564
+ self,
565
+ user_id: str,
566
+ reason: str | None = None,
567
+ ) -> int:
568
+ """
569
+ Terminate all sessions for a user.
570
+
571
+ Args:
572
+ user_id: The user ID.
573
+ reason: Optional reason for termination.
574
+
575
+ Returns:
576
+ Number of sessions terminated.
577
+ """
578
+ with self._lock:
579
+ sessions = self.get_user_sessions(user_id, include_expired=False)
580
+ count = 0
581
+ for session in sessions:
582
+ session.terminate(reason)
583
+ count += 1
584
+ return count
585
+
586
+ def cleanup_expired(self) -> int:
587
+ """
588
+ Remove expired and terminated sessions.
589
+
590
+ Returns:
591
+ Number of sessions removed.
592
+ """
593
+ with self._lock:
594
+ to_remove: list[str] = []
595
+
596
+ for session_id, session in self._sessions.items():
597
+ if session.is_expired() or session.state == SessionState.TERMINATED:
598
+ to_remove.append(session_id)
599
+
600
+ for session_id in to_remove:
601
+ session = self._sessions.pop(session_id, None)
602
+ if session is not None:
603
+ # Remove from user sessions list
604
+ user_id = session.user.user_id
605
+ if user_id in self._user_sessions:
606
+ if session_id in self._user_sessions[user_id]:
607
+ self._user_sessions[user_id].remove(session_id)
608
+
609
+ self._last_cleanup = datetime.now(timezone.utc)
610
+ return len(to_remove)
611
+
612
+ def _maybe_cleanup(self) -> None:
613
+ """Run cleanup if interval has passed."""
614
+ now = datetime.now(timezone.utc)
615
+ elapsed = (now - self._last_cleanup).total_seconds()
616
+ if elapsed >= self.cleanup_interval:
617
+ self.cleanup_expired()
618
+
619
+ def get_active_count(self) -> int:
620
+ """
621
+ Get count of active (non-expired) sessions.
622
+
623
+ Returns:
624
+ Number of active sessions.
625
+ """
626
+ with self._lock:
627
+ count = 0
628
+ for session in self._sessions.values():
629
+ if not session.is_expired() and session.state != SessionState.TERMINATED:
630
+ count += 1
631
+ return count
632
+
633
+ def get_total_count(self) -> int:
634
+ """
635
+ Get total count of sessions (including expired).
636
+
637
+ Returns:
638
+ Total number of sessions.
639
+ """
640
+ with self._lock:
641
+ return len(self._sessions)
642
+
643
+ def get_user_count(self) -> int:
644
+ """
645
+ Get count of unique users with sessions.
646
+
647
+ Returns:
648
+ Number of unique users.
649
+ """
650
+ with self._lock:
651
+ return len(self._user_sessions)
652
+
653
+ def get_stats(self) -> dict[str, Any]:
654
+ """
655
+ Get session statistics.
656
+
657
+ Returns:
658
+ Dictionary with session statistics.
659
+ """
660
+ with self._lock:
661
+ active = 0
662
+ idle = 0
663
+ expired = 0
664
+ terminated = 0
665
+
666
+ for session in self._sessions.values():
667
+ session.is_expired() # Update state
668
+ if session.state == SessionState.ACTIVE:
669
+ active += 1
670
+ elif session.state == SessionState.IDLE:
671
+ idle += 1
672
+ elif session.state == SessionState.EXPIRED:
673
+ expired += 1
674
+ elif session.state == SessionState.TERMINATED:
675
+ terminated += 1
676
+
677
+ return {
678
+ "total": len(self._sessions),
679
+ "active": active,
680
+ "idle": idle,
681
+ "expired": expired,
682
+ "terminated": terminated,
683
+ "unique_users": len(self._user_sessions),
684
+ "last_cleanup": self._last_cleanup.isoformat(),
685
+ }
686
+
687
+ def to_dict(self) -> dict[str, Any]:
688
+ """
689
+ Serialize manager state to dictionary.
690
+
691
+ Returns:
692
+ Dictionary representation of the manager.
693
+ """
694
+ with self._lock:
695
+ return {
696
+ "config": self.config.to_dict(),
697
+ "cleanup_interval": self.cleanup_interval,
698
+ "sessions": {
699
+ sid: session.to_dict()
700
+ for sid, session in self._sessions.items()
701
+ },
702
+ "user_sessions": dict(self._user_sessions),
703
+ "last_cleanup": self._last_cleanup.isoformat(),
704
+ }
705
+
706
+ @classmethod
707
+ def from_dict(cls, data: dict[str, Any]) -> SessionManager:
708
+ """
709
+ Deserialize manager from dictionary.
710
+
711
+ Args:
712
+ data: Dictionary with manager data.
713
+
714
+ Returns:
715
+ SessionManager instance.
716
+ """
717
+ config = SessionConfig.from_dict(data.get("config", {}))
718
+ manager = cls(
719
+ config=config,
720
+ cleanup_interval=data.get("cleanup_interval", 300),
721
+ )
722
+ default_cleanup = datetime.now(timezone.utc).isoformat()
723
+ manager._last_cleanup = datetime.fromisoformat(
724
+ data.get("last_cleanup", default_cleanup)
725
+ )
726
+
727
+ # Restore sessions
728
+ for session_data in data.get("sessions", {}).values():
729
+ session = Session.from_dict(session_data)
730
+ manager._sessions[session.session_id] = session
731
+
732
+ # Restore user sessions mapping
733
+ manager._user_sessions = data.get("user_sessions", {})
734
+
735
+ return manager