chuk-artifacts 0.1.3__py3-none-any.whl → 0.1.5__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.
- chuk_artifacts/admin.py +93 -17
- chuk_artifacts/base.py +9 -3
- chuk_artifacts/batch.py +44 -29
- chuk_artifacts/core.py +18 -17
- chuk_artifacts/metadata.py +24 -22
- chuk_artifacts/presigned.py +24 -23
- chuk_artifacts/store.py +52 -12
- {chuk_artifacts-0.1.3.dist-info → chuk_artifacts-0.1.5.dist-info}/METADATA +2 -2
- chuk_artifacts-0.1.5.dist-info/RECORD +23 -0
- chuk_artifacts/session/__init__.py +0 -0
- chuk_artifacts/session/session_manager.py +0 -196
- chuk_artifacts/session/session_operations.py +0 -366
- chuk_artifacts-0.1.3.dist-info/RECORD +0 -26
- {chuk_artifacts-0.1.3.dist-info → chuk_artifacts-0.1.5.dist-info}/WHEEL +0 -0
- {chuk_artifacts-0.1.3.dist-info → chuk_artifacts-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {chuk_artifacts-0.1.3.dist-info → chuk_artifacts-0.1.5.dist-info}/top_level.txt +0 -0
chuk_artifacts/presigned.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
# chuk_artifacts/presigned.py
|
3
3
|
"""
|
4
4
|
Presigned URL operations: download URLs, upload URLs, and upload registration.
|
5
|
+
Now uses chuk_sessions for session management.
|
5
6
|
"""
|
6
7
|
|
7
8
|
from __future__ import annotations
|
@@ -28,11 +29,11 @@ class PresignedURLOperations:
|
|
28
29
|
"""Handles all presigned URL operations."""
|
29
30
|
|
30
31
|
def __init__(self, artifact_store: 'ArtifactStore'):
|
31
|
-
self.
|
32
|
+
self.artifact_store = artifact_store
|
32
33
|
|
33
34
|
async def presign(self, artifact_id: str, expires: int = _DEFAULT_PRESIGN_EXPIRES) -> str:
|
34
35
|
"""Generate a presigned URL for artifact download."""
|
35
|
-
if self.
|
36
|
+
if self.artifact_store._closed:
|
36
37
|
raise ArtifactStoreError("Store is closed")
|
37
38
|
|
38
39
|
start_time = time.time()
|
@@ -40,11 +41,11 @@ class PresignedURLOperations:
|
|
40
41
|
try:
|
41
42
|
record = await self._get_record(artifact_id)
|
42
43
|
|
43
|
-
storage_ctx_mgr = self.
|
44
|
+
storage_ctx_mgr = self.artifact_store._s3_factory()
|
44
45
|
async with storage_ctx_mgr as s3:
|
45
46
|
url = await s3.generate_presigned_url(
|
46
47
|
"get_object",
|
47
|
-
Params={"Bucket": self.
|
48
|
+
Params={"Bucket": self.artifact_store.bucket, "Key": record["key"]},
|
48
49
|
ExpiresIn=expires,
|
49
50
|
)
|
50
51
|
|
@@ -101,28 +102,28 @@ class PresignedURLOperations:
|
|
101
102
|
expires: int = _DEFAULT_PRESIGN_EXPIRES
|
102
103
|
) -> tuple[str, str]:
|
103
104
|
"""Generate a presigned URL for uploading a new artifact."""
|
104
|
-
if self.
|
105
|
+
if self.artifact_store._closed:
|
105
106
|
raise ArtifactStoreError("Store is closed")
|
106
107
|
|
107
108
|
start_time = time.time()
|
108
109
|
|
109
|
-
# Ensure session is allocated
|
110
|
+
# Ensure session is allocated using chuk_sessions
|
110
111
|
if session_id is None:
|
111
|
-
session_id = await self.
|
112
|
+
session_id = await self.artifact_store._session_manager.allocate_session()
|
112
113
|
else:
|
113
|
-
session_id = await self.
|
114
|
+
session_id = await self.artifact_store._session_manager.allocate_session(session_id=session_id)
|
114
115
|
|
115
116
|
# Generate artifact ID and key path
|
116
117
|
artifact_id = uuid.uuid4().hex
|
117
|
-
key = self.
|
118
|
+
key = self.artifact_store.generate_artifact_key(session_id, artifact_id)
|
118
119
|
|
119
120
|
try:
|
120
|
-
storage_ctx_mgr = self.
|
121
|
+
storage_ctx_mgr = self.artifact_store._s3_factory()
|
121
122
|
async with storage_ctx_mgr as s3:
|
122
123
|
url = await s3.generate_presigned_url(
|
123
124
|
"put_object",
|
124
125
|
Params={
|
125
|
-
"Bucket": self.
|
126
|
+
"Bucket": self.artifact_store.bucket,
|
126
127
|
"Key": key,
|
127
128
|
"ContentType": mime_type
|
128
129
|
},
|
@@ -174,26 +175,26 @@ class PresignedURLOperations:
|
|
174
175
|
ttl: int = _DEFAULT_TTL,
|
175
176
|
) -> bool:
|
176
177
|
"""Register metadata for an artifact uploaded via presigned URL."""
|
177
|
-
if self.
|
178
|
+
if self.artifact_store._closed:
|
178
179
|
raise ArtifactStoreError("Store is closed")
|
179
180
|
|
180
181
|
start_time = time.time()
|
181
182
|
|
182
|
-
# Ensure session is allocated
|
183
|
+
# Ensure session is allocated using chuk_sessions
|
183
184
|
if session_id is None:
|
184
|
-
session_id = await self.
|
185
|
+
session_id = await self.artifact_store._session_manager.allocate_session()
|
185
186
|
else:
|
186
|
-
session_id = await self.
|
187
|
+
session_id = await self.artifact_store._session_manager.allocate_session(session_id=session_id)
|
187
188
|
|
188
189
|
# Reconstruct the key path
|
189
|
-
key = self.
|
190
|
+
key = self.artifact_store.generate_artifact_key(session_id, artifact_id)
|
190
191
|
|
191
192
|
try:
|
192
193
|
# Verify the object exists and get its size
|
193
|
-
storage_ctx_mgr = self.
|
194
|
+
storage_ctx_mgr = self.artifact_store._s3_factory()
|
194
195
|
async with storage_ctx_mgr as s3:
|
195
196
|
try:
|
196
|
-
response = await s3.head_object(Bucket=self.
|
197
|
+
response = await s3.head_object(Bucket=self.artifact_store.bucket, Key=key)
|
197
198
|
file_size = response.get('ContentLength', 0)
|
198
199
|
except Exception:
|
199
200
|
logger.warning(f"Artifact {artifact_id} not found in storage")
|
@@ -203,7 +204,7 @@ class PresignedURLOperations:
|
|
203
204
|
record = {
|
204
205
|
"artifact_id": artifact_id,
|
205
206
|
"session_id": session_id,
|
206
|
-
"sandbox_id": self.
|
207
|
+
"sandbox_id": self.artifact_store.sandbox_id,
|
207
208
|
"key": key,
|
208
209
|
"mime": mime,
|
209
210
|
"summary": summary,
|
@@ -213,13 +214,13 @@ class PresignedURLOperations:
|
|
213
214
|
"sha256": None, # We don't have the hash since we didn't upload it directly
|
214
215
|
"stored_at": datetime.utcnow().isoformat() + "Z",
|
215
216
|
"ttl": ttl,
|
216
|
-
"storage_provider": self.
|
217
|
-
"session_provider": self.
|
217
|
+
"storage_provider": self.artifact_store._storage_provider_name,
|
218
|
+
"session_provider": self.artifact_store._session_provider_name,
|
218
219
|
"uploaded_via_presigned": True, # Flag to indicate upload method
|
219
220
|
}
|
220
221
|
|
221
222
|
# Cache metadata using session provider
|
222
|
-
session_ctx_mgr = self.
|
223
|
+
session_ctx_mgr = self.artifact_store._session_factory()
|
223
224
|
async with session_ctx_mgr as session:
|
224
225
|
await session.setex(artifact_id, ttl, json.dumps(record))
|
225
226
|
|
@@ -288,7 +289,7 @@ class PresignedURLOperations:
|
|
288
289
|
async def _get_record(self, artifact_id: str) -> Dict[str, Any]:
|
289
290
|
"""Get artifact metadata record."""
|
290
291
|
try:
|
291
|
-
session_ctx_mgr = self.
|
292
|
+
session_ctx_mgr = self.artifact_store._session_factory()
|
292
293
|
async with session_ctx_mgr as session:
|
293
294
|
raw = await session.get(artifact_id)
|
294
295
|
except Exception as e:
|
chuk_artifacts/store.py
CHANGED
@@ -7,6 +7,7 @@ Grid Architecture:
|
|
7
7
|
- Mandatory session allocation (no anonymous artifacts)
|
8
8
|
- Grid paths: grid/{sandbox_id}/{session_id}/{artifact_id}
|
9
9
|
- Clean, focused implementation
|
10
|
+
- Now uses chuk_sessions for session management
|
10
11
|
"""
|
11
12
|
|
12
13
|
from __future__ import annotations
|
@@ -32,7 +33,9 @@ except ImportError:
|
|
32
33
|
|
33
34
|
# Import exceptions
|
34
35
|
from .exceptions import ArtifactStoreError, ProviderError
|
35
|
-
|
36
|
+
|
37
|
+
# Import chuk_sessions instead of local session manager
|
38
|
+
from chuk_sessions.session_manager import SessionManager
|
36
39
|
|
37
40
|
# Configure structured logging
|
38
41
|
logger = logging.getLogger(__name__)
|
@@ -64,6 +67,7 @@ class ArtifactStore:
|
|
64
67
|
- Always allocate a session (no anonymous artifacts)
|
65
68
|
- Grid paths only: grid/{sandbox_id}/{session_id}/{artifact_id}
|
66
69
|
- Clean, focused implementation
|
70
|
+
- Uses chuk_sessions for session management
|
67
71
|
"""
|
68
72
|
|
69
73
|
def __init__(
|
@@ -93,10 +97,9 @@ class ArtifactStore:
|
|
93
97
|
self._session_factory = self._load_session_provider(session_provider)
|
94
98
|
self._session_provider_name = session_provider
|
95
99
|
|
96
|
-
# Session manager (
|
100
|
+
# Session manager (now using chuk_sessions)
|
97
101
|
self._session_manager = SessionManager(
|
98
102
|
sandbox_id=self.sandbox_id,
|
99
|
-
session_factory=self._session_factory,
|
100
103
|
default_ttl_hours=session_ttl_hours,
|
101
104
|
)
|
102
105
|
|
@@ -140,17 +143,14 @@ class ArtifactStore:
|
|
140
143
|
ttl: int = _DEFAULT_TTL,
|
141
144
|
) -> str:
|
142
145
|
"""Store artifact with mandatory session allocation."""
|
143
|
-
# Always allocate/validate session
|
146
|
+
# Always allocate/validate session using chuk_sessions
|
144
147
|
session_id = await self._session_manager.allocate_session(
|
145
148
|
session_id=session_id,
|
146
149
|
user_id=user_id,
|
147
150
|
)
|
148
151
|
|
149
|
-
#
|
150
|
-
|
151
|
-
core_store_method = getattr(self._core.__class__, 'store')
|
152
|
-
return await core_store_method(
|
153
|
-
self._core,
|
152
|
+
# Store using core operations
|
153
|
+
return await self._core.store(
|
154
154
|
data=data,
|
155
155
|
mime=mime,
|
156
156
|
summary=summary,
|
@@ -181,18 +181,20 @@ class ArtifactStore:
|
|
181
181
|
return await self._metadata.list_by_session(session_id, limit)
|
182
182
|
|
183
183
|
# ─────────────────────────────────────────────────────────────────
|
184
|
-
# Session operations
|
184
|
+
# Session operations - now delegated to chuk_sessions
|
185
185
|
# ─────────────────────────────────────────────────────────────────
|
186
186
|
|
187
187
|
async def create_session(
|
188
188
|
self,
|
189
189
|
user_id: Optional[str] = None,
|
190
190
|
ttl_hours: Optional[int] = None,
|
191
|
+
custom_metadata: Optional[Dict[str, Any]] = None,
|
191
192
|
) -> str:
|
192
193
|
"""Create a new session."""
|
193
194
|
return await self._session_manager.allocate_session(
|
194
195
|
user_id=user_id,
|
195
196
|
ttl_hours=ttl_hours,
|
197
|
+
custom_metadata=custom_metadata,
|
196
198
|
)
|
197
199
|
|
198
200
|
async def validate_session(self, session_id: str) -> bool:
|
@@ -203,8 +205,32 @@ class ArtifactStore:
|
|
203
205
|
"""Get session information."""
|
204
206
|
return await self._session_manager.get_session_info(session_id)
|
205
207
|
|
208
|
+
async def update_session_metadata(
|
209
|
+
self,
|
210
|
+
session_id: str,
|
211
|
+
metadata: Dict[str, Any]
|
212
|
+
) -> bool:
|
213
|
+
"""Update session metadata."""
|
214
|
+
return await self._session_manager.update_session_metadata(session_id, metadata)
|
215
|
+
|
216
|
+
async def extend_session_ttl(
|
217
|
+
self,
|
218
|
+
session_id: str,
|
219
|
+
additional_hours: int
|
220
|
+
) -> bool:
|
221
|
+
"""Extend session TTL."""
|
222
|
+
return await self._session_manager.extend_session_ttl(session_id, additional_hours)
|
223
|
+
|
224
|
+
async def delete_session(self, session_id: str) -> bool:
|
225
|
+
"""Delete session."""
|
226
|
+
return await self._session_manager.delete_session(session_id)
|
227
|
+
|
228
|
+
async def cleanup_expired_sessions(self) -> int:
|
229
|
+
"""Clean up expired sessions."""
|
230
|
+
return await self._session_manager.cleanup_expired_sessions()
|
231
|
+
|
206
232
|
# ─────────────────────────────────────────────────────────────────
|
207
|
-
# Grid operations
|
233
|
+
# Grid operations - now delegated to chuk_sessions
|
208
234
|
# ─────────────────────────────────────────────────────────────────
|
209
235
|
|
210
236
|
def get_canonical_prefix(self, session_id: str) -> str:
|
@@ -215,6 +241,14 @@ class ArtifactStore:
|
|
215
241
|
"""Generate grid artifact key."""
|
216
242
|
return self._session_manager.generate_artifact_key(session_id, artifact_id)
|
217
243
|
|
244
|
+
def parse_grid_key(self, grid_key: str) -> Optional[Dict[str, Any]]:
|
245
|
+
"""Parse grid key to extract components."""
|
246
|
+
return self._session_manager.parse_grid_key(grid_key)
|
247
|
+
|
248
|
+
def get_session_prefix_pattern(self) -> str:
|
249
|
+
"""Get session prefix pattern for this sandbox."""
|
250
|
+
return self._session_manager.get_session_prefix_pattern()
|
251
|
+
|
218
252
|
# ─────────────────────────────────────────────────────────────────
|
219
253
|
# File operations
|
220
254
|
# ─────────────────────────────────────────────────────────────────
|
@@ -506,7 +540,13 @@ class ArtifactStore:
|
|
506
540
|
|
507
541
|
async def get_stats(self) -> Dict[str, Any]:
|
508
542
|
"""Get storage statistics."""
|
509
|
-
|
543
|
+
stats = await self._admin.get_stats()
|
544
|
+
|
545
|
+
# Add session manager stats
|
546
|
+
session_stats = self._session_manager.get_cache_stats()
|
547
|
+
stats["session_manager"] = session_stats
|
548
|
+
|
549
|
+
return stats
|
510
550
|
|
511
551
|
# ─────────────────────────────────────────────────────────────────
|
512
552
|
# Helpers
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: chuk-artifacts
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.5
|
4
4
|
Summary: Chuk Artifacts provides a production-ready, modular artifact storage system that works seamlessly across multiple storage backends (memory, filesystem, AWS S3, IBM Cloud Object Storage) with Redis or memory-based metadata caching and strict session-based security.
|
5
5
|
License: MIT
|
6
6
|
Requires-Python: >=3.11
|
@@ -12,7 +12,7 @@ Requires-Dist: pyyaml>=6.0.2
|
|
12
12
|
Requires-Dist: aioboto3>=14.3.0
|
13
13
|
Requires-Dist: redis>=6.2.0
|
14
14
|
Requires-Dist: ibm-cos-sdk>=2.13.5
|
15
|
-
Requires-Dist: chuk-sessions>=0.1.
|
15
|
+
Requires-Dist: chuk-sessions>=0.1.1
|
16
16
|
Provides-Extra: websocket
|
17
17
|
Requires-Dist: websockets>=10.0; extra == "websocket"
|
18
18
|
Provides-Extra: dev
|
@@ -0,0 +1,23 @@
|
|
1
|
+
chuk_artifacts/__init__.py,sha256=-4S9FWKVcQSa2ZD3GVbmbpGZPcl0cTQN_TFZLSqV7lQ,3605
|
2
|
+
chuk_artifacts/admin.py,sha256=lUgmKBPxJh-0FYrGWjkACXQOl8lbVEDPJaeGVsWZmC4,6071
|
3
|
+
chuk_artifacts/base.py,sha256=BtuVnC9M8QI1znyTdBxjZ6knIKP_k0yUfLfh7inGJUc,2559
|
4
|
+
chuk_artifacts/batch.py,sha256=x8ARrWJ24I9fAXXodzvh31uMxYrvwZCGGJhUCM4vMJ4,5099
|
5
|
+
chuk_artifacts/config.py,sha256=MaUzHzKPoBUyERviEpv8JVvPybMzSksgLyj0b7AO3Sc,7664
|
6
|
+
chuk_artifacts/core.py,sha256=hokH7cgGE2ZaEwlV8XMKOov3EMvcLS2HufdApLS6l3M,6699
|
7
|
+
chuk_artifacts/exceptions.py,sha256=f-s7Mg7c8vMXsbgqO2B6lMHdXcJQNvsESAY4GhJaV4g,814
|
8
|
+
chuk_artifacts/metadata.py,sha256=McyKLviBInpqh2eF621xhqb3Ix7QUj_nVc1gg_tUlqY,7830
|
9
|
+
chuk_artifacts/models.py,sha256=_foXlkr0DprqgztDw5WtlDc-s1OouLgYNp4XM1Ghp-g,837
|
10
|
+
chuk_artifacts/presigned.py,sha256=-GE8r0CfUZuPNA_jnSGTfX7kuws6kYCPe7C4y6FItdo,11491
|
11
|
+
chuk_artifacts/provider_factory.py,sha256=T0IXx1C8gygJzp417oB44_DxEaZoZR7jcdwQy8FghRE,3398
|
12
|
+
chuk_artifacts/store.py,sha256=3E_eh7JcgyW7-ikLSn_fFMUV4AwN5A0phkEmF0cMaxw,24779
|
13
|
+
chuk_artifacts/providers/__init__.py,sha256=3lN1lAy1ETT1mQslJo1f22PPR1W4CyxmsqJBclzH4NE,317
|
14
|
+
chuk_artifacts/providers/filesystem.py,sha256=F4EjE-_ItPg0RWe7CqameVpOMjU-b7AigEBkm_ZoNrc,15280
|
15
|
+
chuk_artifacts/providers/ibm_cos.py,sha256=K1-VAX4UVV9tA161MOeDXOKloQ0hB77jdw1-p46FwmU,4445
|
16
|
+
chuk_artifacts/providers/ibm_cos_iam.py,sha256=VtwvCi9rMMcZx6i9l21ob6wM8jXseqvjzgCnAA82RkY,3186
|
17
|
+
chuk_artifacts/providers/memory.py,sha256=B1C-tR1PcNz-UuDfGm1bhjPz3oITVATIMPekVbE7nm4,10487
|
18
|
+
chuk_artifacts/providers/s3.py,sha256=eWhBhFSaobpRbazn7ySfU_7D8rm_xCfdSVqRtzXzXRY,2858
|
19
|
+
chuk_artifacts-0.1.5.dist-info/licenses/LICENSE,sha256=SG9BmgtPBagPV0d-Fep-msdAGl-E1CeoBL7-EDRH2qA,1066
|
20
|
+
chuk_artifacts-0.1.5.dist-info/METADATA,sha256=LEDEBlXqdLSHIJDut0xjsBPBHlwLP2X8HfWshD2xyeg,21188
|
21
|
+
chuk_artifacts-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
22
|
+
chuk_artifacts-0.1.5.dist-info/top_level.txt,sha256=1_PVMtWXR0A-ZmeH6apF9mPaMtU0i23JE6wmN4GBRDI,15
|
23
|
+
chuk_artifacts-0.1.5.dist-info/RECORD,,
|
File without changes
|
@@ -1,196 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
# chuk_artifacts/session/session_manager.py
|
3
|
-
"""
|
4
|
-
Clean session manager for grid architecture.
|
5
|
-
|
6
|
-
Simple rules:
|
7
|
-
- Always have a session (auto-allocate if needed)
|
8
|
-
- Grid paths: grid/{sandbox_id}/{session_id}/{artifact_id}
|
9
|
-
- No legacy compatibility, clean implementation
|
10
|
-
"""
|
11
|
-
|
12
|
-
from __future__ import annotations
|
13
|
-
|
14
|
-
import uuid
|
15
|
-
import time
|
16
|
-
import json
|
17
|
-
import asyncio
|
18
|
-
import logging
|
19
|
-
from datetime import datetime, timedelta
|
20
|
-
from typing import Optional, Dict, Any, List, AsyncContextManager, Callable
|
21
|
-
from dataclasses import dataclass, asdict
|
22
|
-
|
23
|
-
from ..exceptions import SessionError, ArtifactStoreError
|
24
|
-
|
25
|
-
logger = logging.getLogger(__name__)
|
26
|
-
|
27
|
-
_DEFAULT_SESSION_TTL_HOURS = 24
|
28
|
-
|
29
|
-
|
30
|
-
@dataclass
|
31
|
-
class SessionMetadata:
|
32
|
-
"""Session metadata for grid operations."""
|
33
|
-
session_id: str
|
34
|
-
sandbox_id: str
|
35
|
-
user_id: Optional[str] = None
|
36
|
-
created_at: str = None
|
37
|
-
expires_at: str = None
|
38
|
-
status: str = "active"
|
39
|
-
artifact_count: int = 0
|
40
|
-
total_bytes: int = 0
|
41
|
-
|
42
|
-
def __post_init__(self):
|
43
|
-
if self.created_at is None:
|
44
|
-
self.created_at = datetime.utcnow().isoformat() + "Z"
|
45
|
-
|
46
|
-
def is_expired(self) -> bool:
|
47
|
-
"""Check if session has expired."""
|
48
|
-
if not self.expires_at:
|
49
|
-
return False
|
50
|
-
expires = datetime.fromisoformat(self.expires_at.replace("Z", ""))
|
51
|
-
return datetime.utcnow() > expires
|
52
|
-
|
53
|
-
def to_dict(self) -> Dict[str, Any]:
|
54
|
-
return asdict(self)
|
55
|
-
|
56
|
-
@classmethod
|
57
|
-
def from_dict(cls, data: Dict[str, Any]) -> 'SessionMetadata':
|
58
|
-
return cls(**data)
|
59
|
-
|
60
|
-
|
61
|
-
class SessionManager:
|
62
|
-
"""Simple session manager for grid architecture."""
|
63
|
-
|
64
|
-
def __init__(
|
65
|
-
self,
|
66
|
-
sandbox_id: str,
|
67
|
-
session_factory: Callable[[], AsyncContextManager],
|
68
|
-
default_ttl_hours: int = _DEFAULT_SESSION_TTL_HOURS,
|
69
|
-
):
|
70
|
-
self.sandbox_id = sandbox_id
|
71
|
-
self.session_factory = session_factory
|
72
|
-
self.default_ttl_hours = default_ttl_hours
|
73
|
-
|
74
|
-
# Simple in-memory cache
|
75
|
-
self._session_cache: Dict[str, SessionMetadata] = {}
|
76
|
-
self._cache_lock = asyncio.Lock()
|
77
|
-
|
78
|
-
logger.info(f"SessionManager initialized for sandbox: {sandbox_id}")
|
79
|
-
|
80
|
-
async def allocate_session(
|
81
|
-
self,
|
82
|
-
session_id: Optional[str] = None,
|
83
|
-
user_id: Optional[str] = None,
|
84
|
-
ttl_hours: Optional[int] = None,
|
85
|
-
) -> str:
|
86
|
-
"""Allocate or validate a session."""
|
87
|
-
ttl_hours = ttl_hours or self.default_ttl_hours
|
88
|
-
|
89
|
-
if session_id:
|
90
|
-
# Validate existing session
|
91
|
-
metadata = await self._get_session_metadata(session_id)
|
92
|
-
if metadata and not metadata.is_expired():
|
93
|
-
await self._touch_session(session_id)
|
94
|
-
return session_id
|
95
|
-
|
96
|
-
# Create new session
|
97
|
-
if not session_id:
|
98
|
-
session_id = self._generate_session_id(user_id)
|
99
|
-
|
100
|
-
expires_at = (datetime.utcnow() + timedelta(hours=ttl_hours)).isoformat() + "Z"
|
101
|
-
|
102
|
-
metadata = SessionMetadata(
|
103
|
-
session_id=session_id,
|
104
|
-
sandbox_id=self.sandbox_id,
|
105
|
-
user_id=user_id,
|
106
|
-
expires_at=expires_at,
|
107
|
-
)
|
108
|
-
|
109
|
-
await self._store_session_metadata(metadata)
|
110
|
-
|
111
|
-
async with self._cache_lock:
|
112
|
-
self._session_cache[session_id] = metadata
|
113
|
-
|
114
|
-
logger.info(f"Session allocated: {session_id} (user: {user_id})")
|
115
|
-
return session_id
|
116
|
-
|
117
|
-
async def validate_session(self, session_id: str) -> bool:
|
118
|
-
"""Check if session is valid."""
|
119
|
-
try:
|
120
|
-
metadata = await self._get_session_metadata(session_id)
|
121
|
-
if metadata and not metadata.is_expired():
|
122
|
-
await self._touch_session(session_id)
|
123
|
-
return True
|
124
|
-
return False
|
125
|
-
except Exception:
|
126
|
-
return False
|
127
|
-
|
128
|
-
async def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
129
|
-
"""Get session information."""
|
130
|
-
metadata = await self._get_session_metadata(session_id)
|
131
|
-
return metadata.to_dict() if metadata else None
|
132
|
-
|
133
|
-
def get_canonical_prefix(self, session_id: str) -> str:
|
134
|
-
"""Get grid path prefix."""
|
135
|
-
return f"grid/{self.sandbox_id}/{session_id}/"
|
136
|
-
|
137
|
-
def generate_artifact_key(self, session_id: str, artifact_id: str) -> str:
|
138
|
-
"""Generate grid artifact key."""
|
139
|
-
return f"grid/{self.sandbox_id}/{session_id}/{artifact_id}"
|
140
|
-
|
141
|
-
def _generate_session_id(self, user_id: Optional[str] = None) -> str:
|
142
|
-
"""Generate session ID."""
|
143
|
-
timestamp = int(time.time())
|
144
|
-
unique = uuid.uuid4().hex[:8]
|
145
|
-
|
146
|
-
if user_id:
|
147
|
-
safe_user = "".join(c for c in user_id if c.isalnum())[:8]
|
148
|
-
return f"sess-{safe_user}-{timestamp}-{unique}"
|
149
|
-
else:
|
150
|
-
return f"sess-{timestamp}-{unique}"
|
151
|
-
|
152
|
-
async def _get_session_metadata(self, session_id: str) -> Optional[SessionMetadata]:
|
153
|
-
"""Get session metadata."""
|
154
|
-
# Check cache
|
155
|
-
async with self._cache_lock:
|
156
|
-
if session_id in self._session_cache:
|
157
|
-
return self._session_cache[session_id]
|
158
|
-
|
159
|
-
# Query session provider
|
160
|
-
try:
|
161
|
-
session_ctx_mgr = self.session_factory()
|
162
|
-
async with session_ctx_mgr as session:
|
163
|
-
raw_data = await session.get(f"session:{session_id}")
|
164
|
-
if raw_data:
|
165
|
-
data = json.loads(raw_data)
|
166
|
-
metadata = SessionMetadata.from_dict(data)
|
167
|
-
|
168
|
-
# Cache it
|
169
|
-
async with self._cache_lock:
|
170
|
-
self._session_cache[session_id] = metadata
|
171
|
-
|
172
|
-
return metadata
|
173
|
-
except Exception as e:
|
174
|
-
logger.warning(f"Failed to get session {session_id}: {e}")
|
175
|
-
|
176
|
-
return None
|
177
|
-
|
178
|
-
async def _store_session_metadata(self, metadata: SessionMetadata) -> None:
|
179
|
-
"""Store session metadata."""
|
180
|
-
try:
|
181
|
-
session_ctx_mgr = self.session_factory()
|
182
|
-
async with session_ctx_mgr as session:
|
183
|
-
key = f"session:{metadata.session_id}"
|
184
|
-
ttl_seconds = int((datetime.fromisoformat(metadata.expires_at.replace("Z", "")) - datetime.utcnow()).total_seconds())
|
185
|
-
data = json.dumps(metadata.to_dict())
|
186
|
-
|
187
|
-
await session.setex(key, ttl_seconds, data)
|
188
|
-
except Exception as e:
|
189
|
-
raise SessionError(f"Session storage failed: {e}") from e
|
190
|
-
|
191
|
-
async def _touch_session(self, session_id: str) -> None:
|
192
|
-
"""Update last accessed time."""
|
193
|
-
metadata = await self._get_session_metadata(session_id)
|
194
|
-
if metadata:
|
195
|
-
# Simple touch - could update last_accessed if we add that field
|
196
|
-
await self._store_session_metadata(metadata)
|