chuk-artifacts 0.3__py3-none-any.whl → 0.4.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.
@@ -5,6 +5,11 @@ Async wrapper for IBM Cloud Object Storage using IAM API-key (oauth).
5
5
 
6
6
  ✓ Fits the aioboto3-style interface that ArtifactStore expects:
7
7
  • async put_object(...)
8
+ • async get_object(...)
9
+ • async head_object(...)
10
+ • async delete_object(...)
11
+ • async list_objects_v2(...)
12
+ • async head_bucket(...)
8
13
  • async generate_presigned_url(...)
9
14
  ✓ No HMAC keys required - just IBM_COS_APIKEY + IBM_COS_INSTANCE_CRN.
10
15
 
@@ -27,6 +32,7 @@ from ibm_botocore.client import Config
27
32
 
28
33
  # ─────────────────────────────────────────────────────────────────────
29
34
  def _sync_client():
35
+ """Create synchronous IBM COS client with IAM authentication."""
30
36
  endpoint = os.getenv(
31
37
  "IBM_COS_ENDPOINT",
32
38
  "https://s3.us-south.cloud-object-storage.appdomain.cloud",
@@ -49,19 +55,52 @@ def _sync_client():
49
55
 
50
56
  # ─────────────────────────────────────────────────────────────────────
51
57
  class _AsyncIBMClient:
52
- """Minimal async façade over synchronous ibm_boto3 S3 client."""
58
+ """Complete async façade over synchronous ibm_boto3 S3 client."""
59
+
53
60
  def __init__(self, sync_client):
54
61
  self._c = sync_client
55
62
 
56
- # ---- methods used by ArtifactStore -------------------------------------
57
- async def put_object(self, **kw) -> Dict[str, Any]:
58
- return await asyncio.to_thread(self._c.put_object, **kw)
63
+ # ---- Core S3 operations used by ArtifactStore -------------------------
64
+ async def put_object(self, **kwargs) -> Dict[str, Any]:
65
+ """Store object in IBM COS."""
66
+ return await asyncio.to_thread(self._c.put_object, **kwargs)
59
67
 
60
- async def generate_presigned_url(self, *a, **kw) -> str:
61
- return await asyncio.to_thread(self._c.generate_presigned_url, *a, **kw)
68
+ async def get_object(self, **kwargs) -> Dict[str, Any]:
69
+ """Retrieve object from IBM COS."""
70
+ return await asyncio.to_thread(self._c.get_object, **kwargs)
62
71
 
63
- # ---- cleanup -----------------------------------------------------------
72
+ async def head_object(self, **kwargs) -> Dict[str, Any]:
73
+ """Get object metadata from IBM COS."""
74
+ return await asyncio.to_thread(self._c.head_object, **kwargs)
75
+
76
+ async def delete_object(self, **kwargs) -> Dict[str, Any]:
77
+ """Delete object from IBM COS."""
78
+ return await asyncio.to_thread(self._c.delete_object, **kwargs)
79
+
80
+ async def list_objects_v2(self, **kwargs) -> Dict[str, Any]:
81
+ """List objects in IBM COS bucket."""
82
+ return await asyncio.to_thread(self._c.list_objects_v2, **kwargs)
83
+
84
+ async def head_bucket(self, **kwargs) -> Dict[str, Any]:
85
+ """Check if bucket exists in IBM COS."""
86
+ return await asyncio.to_thread(self._c.head_bucket, **kwargs)
87
+
88
+ async def generate_presigned_url(self, *args, **kwargs) -> str:
89
+ """Generate presigned URL for IBM COS object."""
90
+ return await asyncio.to_thread(self._c.generate_presigned_url, *args, **kwargs)
91
+
92
+ # ---- Additional operations for completeness ---------------------------
93
+ async def copy_object(self, **kwargs) -> Dict[str, Any]:
94
+ """Copy object within IBM COS."""
95
+ return await asyncio.to_thread(self._c.copy_object, **kwargs)
96
+
97
+ async def delete_objects(self, **kwargs) -> Dict[str, Any]:
98
+ """Delete multiple objects from IBM COS."""
99
+ return await asyncio.to_thread(self._c.delete_objects, **kwargs)
100
+
101
+ # ---- Cleanup -----------------------------------------------------------
64
102
  async def close(self):
103
+ """Close the underlying sync client."""
65
104
  await asyncio.to_thread(self._c.close)
66
105
 
67
106
 
@@ -69,6 +108,11 @@ class _AsyncIBMClient:
69
108
  def factory() -> Callable[[], AsyncContextManager]:
70
109
  """
71
110
  Return a zero-arg callable that yields an async-context-manager.
111
+
112
+ Returns
113
+ -------
114
+ Callable[[], AsyncContextManager]
115
+ Factory function that creates IBM COS IAM client context managers
72
116
  """
73
117
 
74
118
  @asynccontextmanager
@@ -36,6 +36,7 @@ class _MemoryS3Client:
36
36
  If None, creates isolated per-instance storage.
37
37
  """
38
38
  self._store: Dict[str, Dict[str, Any]] = shared_store if shared_store is not None else {}
39
+ self._is_shared_store = shared_store is not None
39
40
  self._lock = asyncio.Lock()
40
41
  self._closed = False
41
42
 
@@ -231,8 +232,11 @@ class _MemoryS3Client:
231
232
  async def close(self):
232
233
  """Clean up resources and mark client as closed."""
233
234
  if not self._closed:
234
- async with self._lock:
235
- self._store.clear()
235
+ # Only clear the store if it's NOT shared
236
+ # If it's shared, other clients may still need the data
237
+ if not self._is_shared_store:
238
+ async with self._lock:
239
+ self._store.clear()
236
240
  self._closed = True
237
241
 
238
242
  # ------------------------------------------------------------
@@ -253,6 +257,10 @@ class _MemoryS3Client:
253
257
  "total_objects": total_objects,
254
258
  "total_bytes": total_bytes,
255
259
  "closed": self._closed,
260
+ "is_shared_store": self._is_shared_store,
261
+ "store_id": id(self._store), # Memory address for debugging
262
+ "client_id": id(self), # Client instance ID
263
+ "store_keys": list(self._store.keys())[:5], # First 5 keys for debugging
256
264
  }
257
265
 
258
266
  @classmethod
@@ -263,17 +271,32 @@ class _MemoryS3Client:
263
271
 
264
272
  # ---- public factory -------------------------------------------------------
265
273
 
274
+ # Global shared storage for memory provider when used as default
275
+ _default_shared_store: Dict[str, Dict[str, Any]] = {}
276
+
266
277
  def factory(shared_store: Optional[Dict[str, Dict[str, Any]]] = None) -> Callable[[], AsyncContextManager]:
267
278
  """
268
279
  Return a **zero-arg** factory that yields an async-context client.
269
280
 
281
+ Key behavior for memory provider:
282
+ - If shared_store is provided, uses that specific storage
283
+ - If shared_store is None, ALWAYS uses the global shared storage
284
+ - This ensures all memory clients in the same process share data
285
+ - Prevents issues where ArtifactStore operations can't see each other's data
286
+
270
287
  Parameters
271
288
  ----------
272
289
  shared_store : dict, optional
273
290
  If provided, all clients created by this factory will share
274
- the same storage dict. Useful for testing scenarios where
275
- you need multiple clients to see the same data.
291
+ the same storage dict. If None, will use a global shared store
292
+ to ensure consistency across operations within the same process.
276
293
  """
294
+
295
+ # CRITICAL: Always use global shared storage when none specified
296
+ # This prevents the common issue where each ArtifactStore operation
297
+ # gets a different isolated storage and can't see each other's data
298
+ if shared_store is None:
299
+ shared_store = _default_shared_store
277
300
 
278
301
  @asynccontextmanager
279
302
  async def _ctx():
@@ -307,6 +330,11 @@ async def clear_all_memory_stores():
307
330
  Emergency cleanup function that clears all active memory stores.
308
331
  Useful for test teardown.
309
332
  """
333
+ # Clear the global shared store
334
+ global _default_shared_store
335
+ _default_shared_store.clear()
336
+
337
+ # Close all active instances
310
338
  instances = list(_MemoryS3Client._instances)
311
339
  for instance in instances:
312
340
  try:
chuk_artifacts/store.py CHANGED
@@ -172,21 +172,24 @@ class ArtifactStore:
172
172
  filename: Optional[str] = None,
173
173
  summary: Optional[str] = None,
174
174
  mime: Optional[str] = None,
175
+ ttl: Optional[int] = None,
175
176
  ) -> bool:
176
177
  """
177
178
  Update an artifact's content, metadata, filename, summary, or mime type.
178
179
  All parameters are optional. At least one must be provided.
179
180
  """
180
- if not any([data, meta, filename, summary, mime]):
181
+ if not any([data is not None, meta is not None, filename is not None,
182
+ summary is not None, mime is not None, ttl is not None]):
181
183
  raise ValueError("At least one update parameter must be provided.")
182
184
 
183
185
  return await self._core.update_file(
184
186
  artifact_id=artifact_id,
185
187
  new_data=data,
188
+ mime=mime,
189
+ summary=summary,
186
190
  meta=meta,
187
191
  filename=filename,
188
- summary=summary,
189
- mime=mime,
192
+ ttl=ttl,
190
193
  )
191
194
 
192
195
  async def retrieve(self, artifact_id: str) -> bytes:
@@ -633,4 +636,50 @@ class ArtifactStore:
633
636
  return self
634
637
 
635
638
  async def __aexit__(self, exc_type, exc_val, exc_tb):
636
- await self.close()
639
+ await self.close()
640
+
641
+ async def get_sandbox_info(self) -> Dict[str, Any]:
642
+ """
643
+ Get sandbox information and metadata.
644
+
645
+ Returns
646
+ -------
647
+ Dict[str, Any]
648
+ Dictionary containing sandbox information including:
649
+ - sandbox_id: The current sandbox identifier
650
+ - bucket: The storage bucket name
651
+ - storage_provider: The storage provider type
652
+ - session_provider: The session provider type
653
+ - session_ttl_hours: Default session TTL
654
+ - grid_prefix_pattern: The grid path pattern for this sandbox
655
+ - created_at: Timestamp of when this info was retrieved
656
+ """
657
+ from datetime import datetime
658
+
659
+ # Get session manager stats if available
660
+ session_stats = {}
661
+ try:
662
+ session_stats = self._session_manager.get_cache_stats()
663
+ except Exception:
664
+ pass # Session manager might not have stats
665
+
666
+ # Get storage stats if available
667
+ storage_stats = {}
668
+ try:
669
+ storage_stats = await self._admin.get_stats()
670
+ except Exception:
671
+ pass # Storage might not have stats
672
+
673
+ return {
674
+ "sandbox_id": self.sandbox_id,
675
+ "bucket": self.bucket,
676
+ "storage_provider": self._storage_provider_name,
677
+ "session_provider": self._session_provider_name,
678
+ "session_ttl_hours": self.session_ttl_hours,
679
+ "max_retries": self.max_retries,
680
+ "grid_prefix_pattern": self.get_session_prefix_pattern(),
681
+ "created_at": datetime.utcnow().isoformat() + "Z",
682
+ "session_stats": session_stats,
683
+ "storage_stats": storage_stats,
684
+ "closed": self._closed,
685
+ }