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.
- chuk_artifacts/__init__.py +5 -1
- chuk_artifacts/core.py +75 -58
- chuk_artifacts/grid.py +140 -7
- chuk_artifacts/metadata.py +1 -0
- chuk_artifacts/models.py +3 -3
- chuk_artifacts/providers/filesystem.py +23 -3
- chuk_artifacts/providers/ibm_cos.py +61 -59
- chuk_artifacts/providers/ibm_cos_iam.py +51 -7
- chuk_artifacts/providers/memory.py +32 -4
- chuk_artifacts/store.py +53 -4
- chuk_artifacts-0.4.1.dist-info/METADATA +730 -0
- chuk_artifacts-0.4.1.dist-info/RECORD +23 -0
- chuk_artifacts-0.3.dist-info/METADATA +0 -719
- chuk_artifacts-0.3.dist-info/RECORD +0 -23
- {chuk_artifacts-0.3.dist-info → chuk_artifacts-0.4.1.dist-info}/WHEEL +0 -0
- {chuk_artifacts-0.3.dist-info → chuk_artifacts-0.4.1.dist-info}/top_level.txt +0 -0
@@ -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
|
-
"""
|
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
|
-
# ----
|
57
|
-
async def put_object(self, **
|
58
|
-
|
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
|
61
|
-
|
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
|
-
|
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
|
-
|
235
|
-
|
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.
|
275
|
-
|
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
|
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
|
-
|
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
|
+
}
|