chuk-artifacts 0.1.3__py3-none-any.whl → 0.1.4__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 +75 -3
- chuk_artifacts/base.py +9 -3
- chuk_artifacts/batch.py +44 -29
- chuk_artifacts/core.py +18 -17
- chuk_artifacts/metadata.py +15 -13
- chuk_artifacts/presigned.py +24 -23
- chuk_artifacts/store.py +52 -12
- {chuk_artifacts-0.1.3.dist-info → chuk_artifacts-0.1.4.dist-info}/METADATA +2 -2
- chuk_artifacts-0.1.4.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.4.dist-info}/WHEEL +0 -0
- {chuk_artifacts-0.1.3.dist-info → chuk_artifacts-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {chuk_artifacts-0.1.3.dist-info → chuk_artifacts-0.1.4.dist-info}/top_level.txt +0 -0
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.4
|
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=O7jQCMbH-ExmYvJkfjHidagOgTF8o32-xQ7d2Ul9o_I,5727
|
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=KinpOF-b8qOYffXx9Ixbv-Ms9MjD7wMtAP03ZVofCsU,7731
|
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.4.dist-info/licenses/LICENSE,sha256=SG9BmgtPBagPV0d-Fep-msdAGl-E1CeoBL7-EDRH2qA,1066
|
20
|
+
chuk_artifacts-0.1.4.dist-info/METADATA,sha256=qtT0mnVKLwYjOhtRaFCY1GDmyrv9eOCC9bjKYDnmYD0,21188
|
21
|
+
chuk_artifacts-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
22
|
+
chuk_artifacts-0.1.4.dist-info/top_level.txt,sha256=1_PVMtWXR0A-ZmeH6apF9mPaMtU0i23JE6wmN4GBRDI,15
|
23
|
+
chuk_artifacts-0.1.4.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)
|