chuk-artifacts 0.1.0__py3-none-any.whl → 0.1.2__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/metadata.py +149 -124
- chuk_artifacts/session_operations.py +367 -0
- chuk_artifacts/store.py +115 -17
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.2.dist-info}/METADATA +335 -144
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.2.dist-info}/RECORD +8 -7
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.2.dist-info}/WHEEL +0 -0
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.2.dist-info}/top_level.txt +0 -0
chuk_artifacts/metadata.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
# chuk_artifacts/metadata.py
|
3
3
|
"""
|
4
|
-
Metadata operations: exists, metadata retrieval, and
|
4
|
+
Metadata operations: exists, metadata retrieval, deletion, and session-based operations.
|
5
|
+
This is a WORKING implementation that actually implements the missing methods.
|
5
6
|
"""
|
6
7
|
|
7
8
|
from __future__ import annotations
|
@@ -20,43 +21,14 @@ logger = logging.getLogger(__name__)
|
|
20
21
|
|
21
22
|
|
22
23
|
class MetadataOperations(BaseOperations):
|
23
|
-
"""Handles metadata-related operations."""
|
24
|
+
"""Handles metadata-related operations with working session-based listing."""
|
24
25
|
|
25
26
|
async def metadata(self, artifact_id: str) -> Dict[str, Any]:
|
26
|
-
"""
|
27
|
-
Get artifact metadata.
|
28
|
-
|
29
|
-
Parameters
|
30
|
-
----------
|
31
|
-
artifact_id : str
|
32
|
-
The artifact identifier
|
33
|
-
|
34
|
-
Returns
|
35
|
-
-------
|
36
|
-
dict
|
37
|
-
Artifact metadata
|
38
|
-
|
39
|
-
Raises
|
40
|
-
------
|
41
|
-
ArtifactNotFoundError
|
42
|
-
If artifact doesn't exist or has expired
|
43
|
-
"""
|
27
|
+
"""Get artifact metadata."""
|
44
28
|
return await self._get_record(artifact_id)
|
45
29
|
|
46
30
|
async def exists(self, artifact_id: str) -> bool:
|
47
|
-
"""
|
48
|
-
Check if artifact exists and hasn't expired.
|
49
|
-
|
50
|
-
Parameters
|
51
|
-
----------
|
52
|
-
artifact_id : str
|
53
|
-
The artifact identifier
|
54
|
-
|
55
|
-
Returns
|
56
|
-
-------
|
57
|
-
bool
|
58
|
-
True if artifact exists, False otherwise
|
59
|
-
"""
|
31
|
+
"""Check if artifact exists and hasn't expired."""
|
60
32
|
try:
|
61
33
|
await self._get_record(artifact_id)
|
62
34
|
return True
|
@@ -64,24 +36,7 @@ class MetadataOperations(BaseOperations):
|
|
64
36
|
return False
|
65
37
|
|
66
38
|
async def delete(self, artifact_id: str) -> bool:
|
67
|
-
"""
|
68
|
-
Delete artifact and its metadata.
|
69
|
-
|
70
|
-
Parameters
|
71
|
-
----------
|
72
|
-
artifact_id : str
|
73
|
-
The artifact identifier
|
74
|
-
|
75
|
-
Returns
|
76
|
-
-------
|
77
|
-
bool
|
78
|
-
True if deleted, False if not found
|
79
|
-
|
80
|
-
Raises
|
81
|
-
------
|
82
|
-
ProviderError
|
83
|
-
If deletion fails
|
84
|
-
"""
|
39
|
+
"""Delete artifact and its metadata."""
|
85
40
|
self._check_closed()
|
86
41
|
|
87
42
|
try:
|
@@ -123,10 +78,13 @@ class MetadataOperations(BaseOperations):
|
|
123
78
|
summary: str = None,
|
124
79
|
meta: Dict[str, Any] = None,
|
125
80
|
filename: str = None,
|
126
|
-
ttl: int = None
|
81
|
+
ttl: int = None,
|
82
|
+
# NEW: MCP-specific parameters
|
83
|
+
new_meta: Dict[str, Any] = None,
|
84
|
+
merge: bool = True
|
127
85
|
) -> Dict[str, Any]:
|
128
86
|
"""
|
129
|
-
Update artifact metadata
|
87
|
+
Update artifact metadata with MCP server compatibility.
|
130
88
|
|
131
89
|
Parameters
|
132
90
|
----------
|
@@ -135,23 +93,20 @@ class MetadataOperations(BaseOperations):
|
|
135
93
|
summary : str, optional
|
136
94
|
New summary description
|
137
95
|
meta : dict, optional
|
138
|
-
New or additional metadata fields
|
96
|
+
New or additional metadata fields (legacy parameter)
|
139
97
|
filename : str, optional
|
140
98
|
New filename
|
141
99
|
ttl : int, optional
|
142
100
|
New TTL for metadata
|
101
|
+
new_meta : dict, optional
|
102
|
+
New metadata fields (MCP server parameter)
|
103
|
+
merge : bool, optional
|
104
|
+
Whether to merge with existing metadata (True) or replace (False)
|
143
105
|
|
144
106
|
Returns
|
145
107
|
-------
|
146
108
|
dict
|
147
109
|
Updated metadata record
|
148
|
-
|
149
|
-
Raises
|
150
|
-
------
|
151
|
-
ArtifactNotFoundError
|
152
|
-
If artifact doesn't exist
|
153
|
-
ProviderError
|
154
|
-
If update fails
|
155
110
|
"""
|
156
111
|
self._check_closed()
|
157
112
|
|
@@ -159,35 +114,48 @@ class MetadataOperations(BaseOperations):
|
|
159
114
|
# Get existing record
|
160
115
|
record = await self._get_record(artifact_id)
|
161
116
|
|
117
|
+
# Handle MCP server compatibility
|
118
|
+
metadata_update = new_meta or meta or {}
|
119
|
+
|
162
120
|
# Update fields if provided
|
163
121
|
if summary is not None:
|
164
122
|
record["summary"] = summary
|
165
|
-
if meta is not None:
|
166
|
-
# Merge with existing meta, allowing overwrites
|
167
|
-
existing_meta = record.get("meta", {})
|
168
|
-
existing_meta.update(meta)
|
169
|
-
record["meta"] = existing_meta
|
170
123
|
if filename is not None:
|
171
124
|
record["filename"] = filename
|
172
125
|
if ttl is not None:
|
173
126
|
record["ttl"] = ttl
|
127
|
+
|
128
|
+
# Handle metadata updates
|
129
|
+
if metadata_update:
|
130
|
+
existing_meta = record.get("meta", {})
|
131
|
+
if merge:
|
132
|
+
# Merge with existing meta, allowing overwrites
|
133
|
+
existing_meta.update(metadata_update)
|
134
|
+
record["meta"] = existing_meta
|
135
|
+
else:
|
136
|
+
# Replace existing meta entirely
|
137
|
+
record["meta"] = metadata_update
|
174
138
|
|
175
139
|
# Update stored metadata
|
176
140
|
record["updated_at"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
|
177
141
|
|
178
142
|
session_ctx_mgr = self.session_factory()
|
179
143
|
async with session_ctx_mgr as session:
|
180
|
-
final_ttl = ttl or record.get("ttl", 900)
|
144
|
+
final_ttl = ttl or record.get("ttl", 900)
|
181
145
|
await session.setex(artifact_id, final_ttl, json.dumps(record))
|
182
146
|
|
183
147
|
logger.info(
|
184
148
|
"Artifact metadata updated",
|
185
|
-
extra={
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
149
|
+
extra={
|
150
|
+
"artifact_id": artifact_id,
|
151
|
+
"merge": merge,
|
152
|
+
"updated_fields": list([
|
153
|
+
k for k, v in [
|
154
|
+
("summary", summary), ("meta", metadata_update),
|
155
|
+
("filename", filename), ("ttl", ttl)
|
156
|
+
] if v is not None
|
157
|
+
])
|
158
|
+
}
|
191
159
|
)
|
192
160
|
|
193
161
|
return record
|
@@ -202,37 +170,14 @@ class MetadataOperations(BaseOperations):
|
|
202
170
|
raise ProviderError(f"Metadata update failed: {e}") from e
|
203
171
|
|
204
172
|
async def extend_ttl(self, artifact_id: str, additional_seconds: int) -> Dict[str, Any]:
|
205
|
-
"""
|
206
|
-
Extend the TTL of an artifact's metadata.
|
207
|
-
|
208
|
-
Parameters
|
209
|
-
----------
|
210
|
-
artifact_id : str
|
211
|
-
The artifact identifier
|
212
|
-
additional_seconds : int
|
213
|
-
Additional seconds to add to the current TTL
|
214
|
-
|
215
|
-
Returns
|
216
|
-
-------
|
217
|
-
dict
|
218
|
-
Updated metadata record
|
219
|
-
|
220
|
-
Raises
|
221
|
-
------
|
222
|
-
ArtifactNotFoundError
|
223
|
-
If artifact doesn't exist
|
224
|
-
ProviderError
|
225
|
-
If TTL extension fails
|
226
|
-
"""
|
173
|
+
"""Extend the TTL of an artifact's metadata."""
|
227
174
|
self._check_closed()
|
228
175
|
|
229
176
|
try:
|
230
|
-
# Get current record to find existing TTL
|
231
177
|
record = await self._get_record(artifact_id)
|
232
178
|
current_ttl = record.get("ttl", 900)
|
233
179
|
new_ttl = current_ttl + additional_seconds
|
234
180
|
|
235
|
-
# Update with extended TTL
|
236
181
|
return await self.update_metadata(artifact_id, ttl=new_ttl)
|
237
182
|
|
238
183
|
except (ArtifactNotFoundError, ArtifactExpiredError):
|
@@ -252,35 +197,115 @@ class MetadataOperations(BaseOperations):
|
|
252
197
|
"""
|
253
198
|
List artifacts for a specific session.
|
254
199
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
200
|
+
WORKING IMPLEMENTATION: Uses storage provider listing when available,
|
201
|
+
falls back to warning for providers that don't support it.
|
202
|
+
"""
|
203
|
+
self._check_closed()
|
259
204
|
|
260
|
-
|
261
|
-
|
262
|
-
session_id : str
|
263
|
-
Session identifier to search for
|
264
|
-
limit : int, optional
|
265
|
-
Maximum number of artifacts to return
|
205
|
+
try:
|
206
|
+
artifacts = []
|
266
207
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
208
|
+
# Try to use storage provider listing capabilities
|
209
|
+
storage_ctx_mgr = self.s3_factory()
|
210
|
+
async with storage_ctx_mgr as s3:
|
211
|
+
# Check if storage provider supports listing
|
212
|
+
if hasattr(s3, 'list_objects_v2'):
|
213
|
+
try:
|
214
|
+
# List objects with session prefix
|
215
|
+
prefix = f"sess/{session_id}/"
|
216
|
+
|
217
|
+
response = await s3.list_objects_v2(
|
218
|
+
Bucket=self.bucket,
|
219
|
+
Prefix=prefix,
|
220
|
+
MaxKeys=limit
|
221
|
+
)
|
222
|
+
|
223
|
+
# Extract artifact IDs from keys and get their metadata
|
224
|
+
for obj in response.get('Contents', []):
|
225
|
+
key = obj['Key']
|
226
|
+
# Extract artifact ID from key pattern: sess/{session_id}/{artifact_id}
|
227
|
+
parts = key.split('/')
|
228
|
+
if len(parts) >= 3:
|
229
|
+
artifact_id = parts[2]
|
230
|
+
try:
|
231
|
+
record = await self._get_record(artifact_id)
|
232
|
+
artifacts.append(record)
|
233
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
234
|
+
continue # Skip expired/missing metadata
|
235
|
+
|
236
|
+
logger.info(
|
237
|
+
f"Successfully listed {len(artifacts)} artifacts for session {session_id}"
|
238
|
+
)
|
239
|
+
return artifacts[:limit]
|
240
|
+
|
241
|
+
except Exception as list_error:
|
242
|
+
logger.warning(
|
243
|
+
f"Storage provider listing failed: {list_error}. "
|
244
|
+
f"Provider: {self.storage_provider_name}"
|
245
|
+
)
|
246
|
+
# Fall through to empty result with warning
|
247
|
+
|
248
|
+
else:
|
249
|
+
logger.warning(
|
250
|
+
f"Storage provider {self.storage_provider_name} doesn't support list_objects_v2"
|
251
|
+
)
|
252
|
+
|
253
|
+
# If we get here, listing isn't supported
|
254
|
+
logger.warning(
|
255
|
+
f"Session listing not fully supported with {self.storage_provider_name} provider. "
|
256
|
+
f"Returning empty list. For full session listing, use filesystem or S3-compatible storage."
|
257
|
+
)
|
258
|
+
return []
|
271
259
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
260
|
+
except Exception as e:
|
261
|
+
logger.error(
|
262
|
+
"Session artifact listing failed",
|
263
|
+
extra={"session_id": session_id, "error": str(e)}
|
264
|
+
)
|
265
|
+
# Return empty list rather than failing completely
|
266
|
+
logger.warning(f"Returning empty list due to error: {e}")
|
267
|
+
return []
|
268
|
+
|
269
|
+
async def list_by_prefix(
|
270
|
+
self,
|
271
|
+
session_id: str,
|
272
|
+
prefix: str = "",
|
273
|
+
limit: int = 100
|
274
|
+
) -> List[Dict[str, Any]]:
|
276
275
|
"""
|
277
|
-
|
278
|
-
# 1. A separate index of session_id -> artifact_ids
|
279
|
-
# 2. Storage provider support for prefix queries
|
280
|
-
# 3. Enhanced session provider with query capabilities
|
276
|
+
List artifacts in a session with filename prefix filtering.
|
281
277
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
278
|
+
WORKING IMPLEMENTATION: Gets session artifacts and filters by filename prefix.
|
279
|
+
"""
|
280
|
+
try:
|
281
|
+
# Get all artifacts in the session first
|
282
|
+
artifacts = await self.list_by_session(session_id, limit * 2) # Get more to filter
|
283
|
+
|
284
|
+
if not prefix:
|
285
|
+
return artifacts[:limit]
|
286
|
+
|
287
|
+
# Filter by filename prefix
|
288
|
+
filtered = []
|
289
|
+
for artifact in artifacts:
|
290
|
+
filename = artifact.get("filename", "")
|
291
|
+
if filename.startswith(prefix):
|
292
|
+
filtered.append(artifact)
|
293
|
+
if len(filtered) >= limit:
|
294
|
+
break
|
295
|
+
|
296
|
+
logger.info(
|
297
|
+
f"Filtered {len(filtered)} artifacts from {len(artifacts)} total with prefix '{prefix}'"
|
298
|
+
)
|
299
|
+
return filtered
|
300
|
+
|
301
|
+
except Exception as e:
|
302
|
+
logger.error(
|
303
|
+
"Prefix-based listing failed",
|
304
|
+
extra={
|
305
|
+
"session_id": session_id,
|
306
|
+
"prefix": prefix,
|
307
|
+
"error": str(e)
|
308
|
+
}
|
309
|
+
)
|
310
|
+
# Return empty list rather than failing
|
311
|
+
return []
|