chuk-artifacts 0.1.1__py3-none-any.whl → 0.1.3__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
@@ -1,12 +1,18 @@
1
1
  # -*- coding: utf-8 -*-
2
- # chuk_artifacts/store.py (ENHANCED)
2
+ # chuk_artifacts/store.py
3
3
  """
4
- Asynchronous, object-store-backed artefact manager with MCP server support.
4
+ Clean ArtifactStore with mandatory sessions and grid architecture.
5
+
6
+ Grid Architecture:
7
+ - Mandatory session allocation (no anonymous artifacts)
8
+ - Grid paths: grid/{sandbox_id}/{session_id}/{artifact_id}
9
+ - Clean, focused implementation
5
10
  """
6
11
 
7
12
  from __future__ import annotations
8
13
 
9
- import os, logging
14
+ import os, logging, uuid
15
+ from datetime import datetime
10
16
  from typing import Any, Dict, List, Callable, AsyncContextManager, Optional, Union
11
17
 
12
18
  try:
@@ -25,7 +31,8 @@ except ImportError:
25
31
  logger.debug("python-dotenv not available, skipping .env file loading")
26
32
 
27
33
  # Import exceptions
28
- from .exceptions import ArtifactStoreError
34
+ from .exceptions import ArtifactStoreError, ProviderError
35
+ from .session.session_manager import SessionManager
29
36
 
30
37
  # Configure structured logging
31
38
  logger = logging.getLogger(__name__)
@@ -51,104 +58,73 @@ def _default_session_factory() -> Callable[[], AsyncContextManager]:
51
58
  # ─────────────────────────────────────────────────────────────────────
52
59
  class ArtifactStore:
53
60
  """
54
- Asynchronous artifact storage with MCP server support.
61
+ Clean ArtifactStore with grid architecture and mandatory sessions.
55
62
 
56
- Enhanced with MCP-specific operations for file management within sessions.
63
+ Simple rules:
64
+ - Always allocate a session (no anonymous artifacts)
65
+ - Grid paths only: grid/{sandbox_id}/{session_id}/{artifact_id}
66
+ - Clean, focused implementation
57
67
  """
58
68
 
59
69
  def __init__(
60
70
  self,
61
71
  *,
62
72
  bucket: Optional[str] = None,
63
- s3_factory: Optional[Callable[[], AsyncContextManager]] = None,
64
73
  storage_provider: Optional[str] = None,
65
- session_factory: Optional[Callable[[], AsyncContextManager]] = None,
66
74
  session_provider: Optional[str] = None,
75
+ sandbox_id: Optional[str] = None,
76
+ session_ttl_hours: int = 24,
67
77
  max_retries: int = 3,
68
- # Backward compatibility - deprecated but still supported
69
- redis_url: Optional[str] = None,
70
- provider: Optional[str] = None,
71
78
  ):
72
- # Read from environment variables with memory as defaults
73
- bucket = bucket or os.getenv("ARTIFACT_BUCKET", "mcp-bucket")
79
+ # Configuration
80
+ self.bucket = bucket or os.getenv("ARTIFACT_BUCKET", "artifacts")
81
+ self.sandbox_id = sandbox_id or self._detect_sandbox_id()
82
+ self.session_ttl_hours = session_ttl_hours
83
+ self.max_retries = max_retries
84
+ self._closed = False
85
+
86
+ # Storage provider
74
87
  storage_provider = storage_provider or os.getenv("ARTIFACT_PROVIDER", "memory")
88
+ self._s3_factory = self._load_storage_provider(storage_provider)
89
+ self._storage_provider_name = storage_provider
90
+
91
+ # Session provider
75
92
  session_provider = session_provider or os.getenv("SESSION_PROVIDER", "memory")
93
+ self._session_factory = self._load_session_provider(session_provider)
94
+ self._session_provider_name = session_provider
76
95
 
77
- # Handle backward compatibility
78
- if redis_url is not None:
79
- import warnings
80
- warnings.warn(
81
- "redis_url parameter is deprecated. Use session_provider='redis' "
82
- "and set SESSION_REDIS_URL environment variable instead.",
83
- DeprecationWarning,
84
- stacklevel=2
85
- )
86
- os.environ["SESSION_REDIS_URL"] = redis_url
87
- session_provider = "redis"
88
-
89
- if provider is not None:
90
- import warnings
91
- warnings.warn(
92
- "provider parameter is deprecated. Use storage_provider instead.",
93
- DeprecationWarning,
94
- stacklevel=2
95
- )
96
- storage_provider = provider
97
-
98
- # Validate factory/provider combinations
99
- if s3_factory and storage_provider:
100
- raise ValueError("Specify either s3_factory or storage_provider—not both")
101
- if session_factory and session_provider:
102
- raise ValueError("Specify either session_factory or session_provider—not both")
103
-
104
- # Initialize storage factory
105
- if s3_factory:
106
- self._s3_factory = s3_factory
107
- elif storage_provider:
108
- self._s3_factory = self._load_storage_provider(storage_provider)
109
- else:
110
- self._s3_factory = _default_storage_factory()
111
-
112
- # Initialize session factory
113
- if session_factory:
114
- self._session_factory = session_factory
115
- elif session_provider:
116
- self._session_factory = self._load_session_provider(session_provider)
117
- else:
118
- self._session_factory = _default_session_factory()
119
-
120
- self.bucket = bucket
121
- self.max_retries = max_retries
122
- self._storage_provider_name = storage_provider or "memory"
123
- self._session_provider_name = session_provider or "memory"
124
- self._closed = False
125
-
126
- # Initialize operation modules
127
- from .core import CoreStorageOperations
128
- from .presigned import PresignedURLOperations
129
- from .metadata import MetadataOperations
130
- from .batch import BatchOperations
131
- from .admin import AdminOperations
132
- from .session_operations import SessionOperations
96
+ # Session manager (always enabled)
97
+ self._session_manager = SessionManager(
98
+ sandbox_id=self.sandbox_id,
99
+ session_factory=self._session_factory,
100
+ default_ttl_hours=session_ttl_hours,
101
+ )
102
+
103
+ # Operation modules
104
+ from .core import CoreStorageOperations as CoreOps
105
+ from .metadata import MetadataOperations as MetaOps
106
+ from .presigned import PresignedURLOperations as PresignedOps
107
+ from .batch import BatchOperations as BatchOps
108
+ from .admin import AdminOperations as AdminOps
109
+
110
+ self._core = CoreOps(self)
111
+ self._metadata = MetaOps(self)
112
+ self._presigned = PresignedOps(self)
113
+ self._batch = BatchOps(self)
114
+ self._admin = AdminOps(self)
133
115
 
134
- self._core = CoreStorageOperations(self)
135
- self._presigned = PresignedURLOperations(self)
136
- self._metadata = MetadataOperations(self)
137
- self._batch = BatchOperations(self)
138
- self._admin = AdminOperations(self)
139
- self._session = SessionOperations(self)
140
-
141
116
  logger.info(
142
- "ArtifactStore initialized with session operations support",
117
+ "ArtifactStore initialized",
143
118
  extra={
144
- "bucket": bucket,
145
- "storage_provider": self._storage_provider_name,
146
- "session_provider": self._session_provider_name,
119
+ "bucket": self.bucket,
120
+ "sandbox_id": self.sandbox_id,
121
+ "storage_provider": storage_provider,
122
+ "session_provider": session_provider,
147
123
  }
148
124
  )
149
125
 
150
126
  # ─────────────────────────────────────────────────────────────────
151
- # Core storage operations (delegated to CoreStorageOperations)
127
+ # Core operations
152
128
  # ─────────────────────────────────────────────────────────────────
153
129
 
154
130
  async def store(
@@ -160,11 +136,22 @@ class ArtifactStore:
160
136
  meta: Dict[str, Any] | None = None,
161
137
  filename: str | None = None,
162
138
  session_id: str | None = None,
139
+ user_id: str | None = None,
163
140
  ttl: int = _DEFAULT_TTL,
164
141
  ) -> str:
165
- """Store artifact data with metadata."""
166
- return await self._core.store(
167
- data,
142
+ """Store artifact with mandatory session allocation."""
143
+ # Always allocate/validate session
144
+ session_id = await self._session_manager.allocate_session(
145
+ session_id=session_id,
146
+ user_id=user_id,
147
+ )
148
+
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,
154
+ data=data,
168
155
  mime=mime,
169
156
  summary=summary,
170
157
  meta=meta,
@@ -174,11 +161,226 @@ class ArtifactStore:
174
161
  )
175
162
 
176
163
  async def retrieve(self, artifact_id: str) -> bytes:
177
- """Retrieve artifact data directly."""
164
+ """Retrieve artifact data."""
178
165
  return await self._core.retrieve(artifact_id)
179
166
 
167
+ async def metadata(self, artifact_id: str) -> Dict[str, Any]:
168
+ """Get artifact metadata."""
169
+ return await self._metadata.get_metadata(artifact_id)
170
+
171
+ async def exists(self, artifact_id: str) -> bool:
172
+ """Check if artifact exists."""
173
+ return await self._metadata.exists(artifact_id)
174
+
175
+ async def delete(self, artifact_id: str) -> bool:
176
+ """Delete artifact."""
177
+ return await self._metadata.delete(artifact_id)
178
+
179
+ async def list_by_session(self, session_id: str, limit: int = 100) -> List[Dict[str, Any]]:
180
+ """List artifacts in session."""
181
+ return await self._metadata.list_by_session(session_id, limit)
182
+
183
+ # ─────────────────────────────────────────────────────────────────
184
+ # Session operations
185
+ # ─────────────────────────────────────────────────────────────────
186
+
187
+ async def create_session(
188
+ self,
189
+ user_id: Optional[str] = None,
190
+ ttl_hours: Optional[int] = None,
191
+ ) -> str:
192
+ """Create a new session."""
193
+ return await self._session_manager.allocate_session(
194
+ user_id=user_id,
195
+ ttl_hours=ttl_hours,
196
+ )
197
+
198
+ async def validate_session(self, session_id: str) -> bool:
199
+ """Validate session."""
200
+ return await self._session_manager.validate_session(session_id)
201
+
202
+ async def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
203
+ """Get session information."""
204
+ return await self._session_manager.get_session_info(session_id)
205
+
206
+ # ─────────────────────────────────────────────────────────────────
207
+ # Grid operations
208
+ # ─────────────────────────────────────────────────────────────────
209
+
210
+ def get_canonical_prefix(self, session_id: str) -> str:
211
+ """Get grid path prefix for session."""
212
+ return self._session_manager.get_canonical_prefix(session_id)
213
+
214
+ def generate_artifact_key(self, session_id: str, artifact_id: str) -> str:
215
+ """Generate grid artifact key."""
216
+ return self._session_manager.generate_artifact_key(session_id, artifact_id)
217
+
218
+ # ─────────────────────────────────────────────────────────────────
219
+ # File operations
220
+ # ─────────────────────────────────────────────────────────────────
221
+
222
+ async def write_file(
223
+ self,
224
+ content: Union[str, bytes],
225
+ *,
226
+ filename: str,
227
+ mime: str = "text/plain",
228
+ summary: str = "",
229
+ session_id: str = None,
230
+ user_id: str = None,
231
+ meta: Dict[str, Any] = None,
232
+ encoding: str = "utf-8",
233
+ ) -> str:
234
+ """Write content to file."""
235
+ if isinstance(content, str):
236
+ data = content.encode(encoding)
237
+ else:
238
+ data = content
239
+
240
+ return await self.store(
241
+ data=data,
242
+ mime=mime,
243
+ summary=summary or f"File: {filename}",
244
+ filename=filename,
245
+ session_id=session_id,
246
+ user_id=user_id,
247
+ meta=meta,
248
+ )
249
+
250
+ async def read_file(
251
+ self,
252
+ artifact_id: str,
253
+ *,
254
+ encoding: str = "utf-8",
255
+ as_text: bool = True
256
+ ) -> Union[str, bytes]:
257
+ """Read file content."""
258
+ data = await self.retrieve(artifact_id)
259
+
260
+ if as_text:
261
+ return data.decode(encoding)
262
+ return data
263
+
264
+ async def list_files(
265
+ self,
266
+ session_id: str,
267
+ prefix: str = "",
268
+ limit: int = 100
269
+ ) -> List[Dict[str, Any]]:
270
+ """List files in session with optional prefix filter."""
271
+ return await self._metadata.list_by_prefix(session_id, prefix, limit)
272
+
273
+ async def get_directory_contents(
274
+ self,
275
+ session_id: str,
276
+ directory_prefix: str = "",
277
+ limit: int = 100
278
+ ) -> List[Dict[str, Any]]:
279
+ """
280
+ List files in a directory-like structure within a session.
281
+ """
282
+ try:
283
+ return await self._metadata.list_by_prefix(session_id, directory_prefix, limit)
284
+ except Exception as e:
285
+ logger.error(
286
+ "Directory listing failed for session %s: %s",
287
+ session_id,
288
+ str(e),
289
+ extra={
290
+ "session_id": session_id,
291
+ "directory_prefix": directory_prefix,
292
+ "operation": "get_directory_contents"
293
+ }
294
+ )
295
+ raise ProviderError(f"Directory listing failed: {e}") from e
296
+
297
+ async def copy_file(
298
+ self,
299
+ artifact_id: str,
300
+ *,
301
+ new_filename: str = None,
302
+ target_session_id: str = None,
303
+ new_meta: Dict[str, Any] = None,
304
+ summary: str = None
305
+ ) -> str:
306
+ """Copy a file WITHIN THE SAME SESSION only (security enforced)."""
307
+ # Get original metadata to check session
308
+ original_meta = await self.metadata(artifact_id)
309
+ original_session = original_meta.get("session_id")
310
+
311
+ # STRICT SECURITY: Block ALL cross-session copies
312
+ if target_session_id and target_session_id != original_session:
313
+ raise ArtifactStoreError(
314
+ f"Cross-session copies are not permitted for security reasons. "
315
+ f"Artifact {artifact_id} belongs to session '{original_session}', "
316
+ f"cannot copy to session '{target_session_id}'. Files can only be "
317
+ f"copied within the same session."
318
+ )
319
+
320
+ # Get original data
321
+ original_data = await self.retrieve(artifact_id)
322
+
323
+ # Prepare copy metadata
324
+ copy_filename = new_filename or (
325
+ (original_meta.get("filename", "file") or "file") + "_copy"
326
+ )
327
+ copy_summary = summary or f"Copy of {original_meta.get('summary', 'artifact')}"
328
+
329
+ # Merge metadata
330
+ copy_meta = {**original_meta.get("meta", {})}
331
+ if new_meta:
332
+ copy_meta.update(new_meta)
333
+
334
+ # Add copy tracking
335
+ copy_meta["copied_from"] = artifact_id
336
+ copy_meta["copy_timestamp"] = datetime.utcnow().isoformat() + "Z"
337
+
338
+ # Store the copy in the same session
339
+ return await self.store(
340
+ data=original_data,
341
+ mime=original_meta["mime"],
342
+ summary=copy_summary,
343
+ filename=copy_filename,
344
+ session_id=original_session, # Always same session
345
+ meta=copy_meta
346
+ )
347
+
348
+ async def move_file(
349
+ self,
350
+ artifact_id: str,
351
+ *,
352
+ new_filename: str = None,
353
+ new_session_id: str = None,
354
+ new_meta: Dict[str, Any] = None
355
+ ) -> Dict[str, Any]:
356
+ """Move/rename a file WITHIN THE SAME SESSION only (security enforced)."""
357
+ # Get current metadata
358
+ record = await self.metadata(artifact_id)
359
+ current_session = record.get("session_id")
360
+
361
+ # STRICT SECURITY: Block ALL cross-session moves
362
+ if new_session_id and new_session_id != current_session:
363
+ raise ArtifactStoreError(
364
+ f"Cross-session moves are not permitted for security reasons. "
365
+ f"Artifact {artifact_id} belongs to session '{current_session}', "
366
+ f"cannot move to session '{new_session_id}'. Use copy operations within "
367
+ f"the same session only."
368
+ )
369
+
370
+ # For now, just simulate a move by updating metadata
371
+ # A full implementation would update the metadata record
372
+ if new_filename:
373
+ # This is a simplified move - just return updated record
374
+ record["filename"] = new_filename
375
+ if new_meta:
376
+ existing_meta = record.get("meta", {})
377
+ existing_meta.update(new_meta)
378
+ record["meta"] = existing_meta
379
+
380
+ return record
381
+
180
382
  # ─────────────────────────────────────────────────────────────────
181
- # Presigned URL operations (delegated to PresignedURLOperations)
383
+ # Presigned URL operations
182
384
  # ─────────────────────────────────────────────────────────────────
183
385
 
184
386
  async def presign(self, artifact_id: str, expires: int = _DEFAULT_PRESIGN_EXPIRES) -> str:
@@ -205,12 +407,7 @@ class ArtifactStore:
205
407
  expires: int = _DEFAULT_PRESIGN_EXPIRES
206
408
  ) -> tuple[str, str]:
207
409
  """Generate a presigned URL for uploading a new artifact."""
208
- return await self._presigned.presign_upload(
209
- session_id=session_id,
210
- filename=filename,
211
- mime_type=mime_type,
212
- expires=expires
213
- )
410
+ return await self._presigned.presign_upload(session_id, filename, mime_type, expires)
214
411
 
215
412
  async def register_uploaded_artifact(
216
413
  self,
@@ -257,76 +454,50 @@ class ArtifactStore:
257
454
  )
258
455
 
259
456
  # ─────────────────────────────────────────────────────────────────
260
- # Metadata operations (delegated to MetadataOperations)
457
+ # Batch operations
261
458
  # ─────────────────────────────────────────────────────────────────
262
459
 
263
- async def metadata(self, artifact_id: str) -> Dict[str, Any]:
264
- """Get artifact metadata."""
265
- return await self._metadata.metadata(artifact_id)
266
-
267
- async def exists(self, artifact_id: str) -> bool:
268
- """Check if artifact exists and hasn't expired."""
269
- return await self._metadata.exists(artifact_id)
460
+ async def store_batch(
461
+ self,
462
+ items: List[Dict[str, Any]],
463
+ session_id: str | None = None,
464
+ ttl: int = _DEFAULT_TTL,
465
+ ) -> List[str]:
466
+ """Store multiple artifacts in a batch operation."""
467
+ return await self._batch.store_batch(items, session_id, ttl)
270
468
 
271
- async def delete(self, artifact_id: str) -> bool:
272
- """Delete artifact and its metadata."""
273
- return await self._metadata.delete(artifact_id)
469
+ # ─────────────────────────────────────────────────────────────────
470
+ # Metadata operations
471
+ # ─────────────────────────────────────────────────────────────────
274
472
 
275
473
  async def update_metadata(
276
- self,
277
- artifact_id: str,
474
+ self,
475
+ artifact_id: str,
278
476
  *,
279
477
  summary: str = None,
280
478
  meta: Dict[str, Any] = None,
281
- filename: str = None,
282
- ttl: int = None,
283
- # NEW: MCP-specific parameters
284
- new_meta: Dict[str, Any] = None,
285
- merge: bool = True
479
+ merge: bool = True,
480
+ **kwargs
286
481
  ) -> Dict[str, Any]:
287
- """Update artifact metadata with MCP server compatibility."""
482
+ """Update artifact metadata."""
288
483
  return await self._metadata.update_metadata(
289
484
  artifact_id,
290
485
  summary=summary,
291
486
  meta=meta,
292
- filename=filename,
293
- ttl=ttl,
294
- new_meta=new_meta,
295
- merge=merge
487
+ merge=merge,
488
+ **kwargs
296
489
  )
297
490
 
298
- async def extend_ttl(self, artifact_id: str, additional_seconds: int) -> Dict[str, Any]:
299
- """Extend the TTL of an artifact's metadata."""
300
- return await self._metadata.extend_ttl(artifact_id, additional_seconds)
301
-
302
- async def list_by_session(self, session_id: str, limit: int = 100) -> List[Dict[str, Any]]:
303
- """List artifacts for a specific session."""
304
- return await self._metadata.list_by_session(session_id, limit)
305
-
306
- async def list_by_prefix(
307
- self,
308
- session_id: str,
309
- prefix: str = "",
310
- limit: int = 100
311
- ) -> List[Dict[str, Any]]:
312
- """List artifacts in a session with filename prefix filtering."""
313
- return await self._metadata.list_by_prefix(session_id, prefix, limit)
314
-
315
- # ─────────────────────────────────────────────────────────────────
316
- # Batch operations (delegated to BatchOperations)
317
- # ─────────────────────────────────────────────────────────────────
318
-
319
- async def store_batch(
491
+ async def extend_ttl(
320
492
  self,
321
- items: List[Dict[str, Any]],
322
- session_id: str | None = None,
323
- ttl: int = _DEFAULT_TTL,
324
- ) -> List[str]:
325
- """Store multiple artifacts in a batch operation."""
326
- return await self._batch.store_batch(items, session_id, ttl)
493
+ artifact_id: str,
494
+ additional_seconds: int
495
+ ) -> Dict[str, Any]:
496
+ """Extend artifact TTL."""
497
+ return await self._metadata.extend_ttl(artifact_id, additional_seconds)
327
498
 
328
499
  # ─────────────────────────────────────────────────────────────────
329
- # Administrative operations (delegated to AdminOperations)
500
+ # Administrative operations
330
501
  # ─────────────────────────────────────────────────────────────────
331
502
 
332
503
  async def validate_configuration(self) -> Dict[str, Any]:
@@ -338,100 +509,53 @@ class ArtifactStore:
338
509
  return await self._admin.get_stats()
339
510
 
340
511
  # ─────────────────────────────────────────────────────────────────
341
- # Session-based file operations (delegated to SessionOperations)
512
+ # Helpers
342
513
  # ─────────────────────────────────────────────────────────────────
343
514
 
344
- async def move_file(
345
- self,
346
- artifact_id: str,
347
- *,
348
- new_filename: str = None,
349
- new_session_id: str = None,
350
- new_meta: Dict[str, Any] = None
351
- ) -> Dict[str, Any]:
352
- """Move a file within sessions or rename it."""
353
- return await self._session.move_file(
354
- artifact_id,
355
- new_filename=new_filename,
356
- new_session_id=new_session_id,
357
- new_meta=new_meta
358
- )
359
-
360
- async def copy_file(
361
- self,
362
- artifact_id: str,
363
- *,
364
- new_filename: str = None,
365
- target_session_id: str = None,
366
- new_meta: Dict[str, Any] = None,
367
- summary: str = None
368
- ) -> str:
369
- """Copy a file within or across sessions."""
370
- return await self._session.copy_file(
371
- artifact_id,
372
- new_filename=new_filename,
373
- target_session_id=target_session_id,
374
- new_meta=new_meta,
375
- summary=summary
376
- )
377
-
378
- async def read_file(
379
- self,
380
- artifact_id: str,
381
- *,
382
- encoding: str = "utf-8",
383
- as_text: bool = True
384
- ) -> Union[str, bytes]:
385
- """Read file content directly."""
386
- return await self._session.read_file(
387
- artifact_id,
388
- encoding=encoding,
389
- as_text=as_text
390
- )
515
+ def _detect_sandbox_id(self) -> str:
516
+ """Auto-detect sandbox ID."""
517
+ candidates = [
518
+ os.getenv("ARTIFACT_SANDBOX_ID"),
519
+ os.getenv("SANDBOX_ID"),
520
+ os.getenv("HOSTNAME"),
521
+ ]
522
+
523
+ for candidate in candidates:
524
+ if candidate:
525
+ clean_id = "".join(c for c in candidate if c.isalnum() or c in "-_")[:32]
526
+ if clean_id:
527
+ return clean_id
528
+
529
+ # Generate fallback
530
+ return f"sandbox-{uuid.uuid4().hex[:8]}"
391
531
 
392
- async def write_file(
393
- self,
394
- content: Union[str, bytes],
395
- *,
396
- filename: str,
397
- mime: str = "text/plain",
398
- summary: str = "",
399
- session_id: str = None,
400
- meta: Dict[str, Any] = None,
401
- encoding: str = "utf-8",
402
- overwrite_artifact_id: str = None
403
- ) -> str:
404
- """Write content to a new file or overwrite existing."""
405
- return await self._session.write_file(
406
- content,
407
- filename=filename,
408
- mime=mime,
409
- summary=summary,
410
- session_id=session_id,
411
- meta=meta,
412
- encoding=encoding,
413
- overwrite_artifact_id=overwrite_artifact_id
414
- )
532
+ def _load_storage_provider(self, name: str) -> Callable[[], AsyncContextManager]:
533
+ """Load storage provider."""
534
+ from importlib import import_module
535
+
536
+ try:
537
+ mod = import_module(f"chuk_artifacts.providers.{name}")
538
+ return mod.factory()
539
+ except ModuleNotFoundError as exc:
540
+ available = ["memory", "filesystem", "s3", "ibm_cos"]
541
+ raise ValueError(f"Unknown storage provider '{name}'. Available: {', '.join(available)}") from exc
415
542
 
416
- async def get_directory_contents(
417
- self,
418
- session_id: str,
419
- directory_prefix: str = "",
420
- limit: int = 100
421
- ) -> List[Dict[str, Any]]:
422
- """List files in a directory-like structure within a session."""
423
- return await self._session.get_directory_contents(
424
- session_id,
425
- directory_prefix,
426
- limit
427
- )
543
+ def _load_session_provider(self, name: str) -> Callable[[], AsyncContextManager]:
544
+ """Load session provider."""
545
+ from importlib import import_module
546
+
547
+ try:
548
+ mod = import_module(f"chuk_sessions.providers.{name}")
549
+ return mod.factory()
550
+ except ModuleNotFoundError as exc:
551
+ raise ValueError(f"Unknown session provider '{name}'") from exc
428
552
 
429
553
  # ─────────────────────────────────────────────────────────────────
430
554
  # Resource management
431
555
  # ─────────────────────────────────────────────────────────────────
432
556
 
433
557
  async def close(self):
434
- """Mark store as closed."""
558
+ """Close the store."""
435
559
  if not self._closed:
436
560
  self._closed = True
437
561
  logger.info("ArtifactStore closed")
@@ -440,42 +564,4 @@ class ArtifactStore:
440
564
  return self
441
565
 
442
566
  async def __aexit__(self, exc_type, exc_val, exc_tb):
443
- await self.close()
444
-
445
- # ─────────────────────────────────────────────────────────────────
446
- # Helper functions (still needed for provider loading)
447
- # ─────────────────────────────────────────────────────────────────
448
-
449
- def _load_storage_provider(self, name: str) -> Callable[[], AsyncContextManager]:
450
- """Load storage provider by name."""
451
- from importlib import import_module
452
-
453
- try:
454
- mod = import_module(f"chuk_artifacts.providers.{name}")
455
- except ModuleNotFoundError as exc:
456
- available = ["memory", "filesystem", "s3", "ibm_cos", "ibm_cos_iam"]
457
- raise ValueError(
458
- f"Unknown storage provider '{name}'. "
459
- f"Available providers: {', '.join(available)}"
460
- ) from exc
461
-
462
- if not hasattr(mod, "factory"):
463
- raise AttributeError(f"Storage provider '{name}' lacks factory()")
464
-
465
- logger.info(f"Loaded storage provider: {name}")
466
- return mod.factory()
467
-
468
- def _load_session_provider(self, name: str) -> Callable[[], AsyncContextManager]:
469
- """Load session provider by name."""
470
- from importlib import import_module
471
-
472
- try:
473
- mod = import_module(f"chuk_sessions.providers.{name}")
474
- except ModuleNotFoundError as exc:
475
- raise ValueError(f"Unknown session provider '{name}'") from exc
476
-
477
- if not hasattr(mod, "factory"):
478
- raise AttributeError(f"Session provider '{name}' lacks factory()")
479
-
480
- logger.info(f"Loaded session provider: {name}")
481
- return mod.factory()
567
+ await self.close()