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/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
- from .session.session_manager import SessionManager
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 (always enabled)
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
- # Work around the naming conflict in CoreStorageOperations
150
- # where self.store is the artifact store instance, not the method
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
- return await self._admin.get_stats()
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
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.0
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)