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.
@@ -1,7 +1,8 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # chuk_artifacts/metadata.py
3
3
  """
4
- Metadata operations: exists, metadata retrieval, and deletion.
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 without changing the stored data.
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) # Use provided TTL or existing/default
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={"artifact_id": artifact_id, "updated_fields": list([
186
- k for k, v in [
187
- ("summary", summary), ("meta", meta),
188
- ("filename", filename), ("ttl", ttl)
189
- ] if v is not None
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
- Note: This is a basic implementation that would need to be enhanced
256
- with proper indexing for production use. Currently, this method
257
- cannot be efficiently implemented with the session provider abstraction
258
- since we don't have a way to query by session_id patterns.
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
- Parameters
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
- Returns
268
- -------
269
- list
270
- List of metadata records for artifacts in the session
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
- Raises
273
- ------
274
- NotImplementedError
275
- This method requires additional indexing infrastructure
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
- # This would require either:
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
- raise NotImplementedError(
283
- "list_by_session requires additional indexing infrastructure. "
284
- "Consider implementing session-based indexing or using storage "
285
- "provider list operations if available."
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 []