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
@@ -0,0 +1,367 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# chuk_artifacts/session_operations.py (LOGGING FIX)
|
3
|
+
"""
|
4
|
+
Session-based file operations with strict session isolation.
|
5
|
+
FIXED: Resolved logging conflict with 'filename' parameter.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
import uuid, hashlib, json, logging
|
11
|
+
from datetime import datetime
|
12
|
+
from typing import Any, Dict, Optional, Union, List
|
13
|
+
|
14
|
+
from .base import BaseOperations
|
15
|
+
from .exceptions import (
|
16
|
+
ArtifactStoreError, ArtifactNotFoundError, ArtifactExpiredError,
|
17
|
+
ProviderError, SessionError
|
18
|
+
)
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
_ANON_PREFIX = "anon"
|
23
|
+
_DEFAULT_TTL = 900
|
24
|
+
|
25
|
+
|
26
|
+
class SessionOperations(BaseOperations):
|
27
|
+
"""Session-based file operations with strict session isolation."""
|
28
|
+
|
29
|
+
async def move_file(
|
30
|
+
self,
|
31
|
+
artifact_id: str,
|
32
|
+
*,
|
33
|
+
new_filename: str = None,
|
34
|
+
new_session_id: str = None,
|
35
|
+
new_meta: Dict[str, Any] = None
|
36
|
+
) -> Dict[str, Any]:
|
37
|
+
"""
|
38
|
+
Move a file within the SAME session or rename it.
|
39
|
+
"""
|
40
|
+
self._check_closed()
|
41
|
+
|
42
|
+
try:
|
43
|
+
# Get current metadata
|
44
|
+
record = await self._get_record(artifact_id)
|
45
|
+
current_session = record.get("session_id")
|
46
|
+
|
47
|
+
# STRICT SECURITY: Block ALL cross-session moves
|
48
|
+
if new_session_id and new_session_id != current_session:
|
49
|
+
raise ArtifactStoreError(
|
50
|
+
f"Cross-session moves are not permitted for security reasons. "
|
51
|
+
f"Artifact {artifact_id} belongs to session '{current_session}', "
|
52
|
+
f"cannot move to session '{new_session_id}'. Use copy operations within "
|
53
|
+
f"the same session only."
|
54
|
+
)
|
55
|
+
|
56
|
+
# Update metadata fields (only filename and meta allowed)
|
57
|
+
updates = {}
|
58
|
+
if new_filename:
|
59
|
+
updates["filename"] = new_filename
|
60
|
+
if new_meta:
|
61
|
+
existing_meta = record.get("meta", {})
|
62
|
+
existing_meta.update(new_meta)
|
63
|
+
updates["new_meta"] = existing_meta
|
64
|
+
updates["merge"] = True
|
65
|
+
|
66
|
+
if updates:
|
67
|
+
# Use the metadata operations to update
|
68
|
+
from .metadata import MetadataOperations
|
69
|
+
metadata_ops = MetadataOperations(self._artifact_store)
|
70
|
+
return await metadata_ops.update_metadata(artifact_id, **updates)
|
71
|
+
|
72
|
+
return record
|
73
|
+
|
74
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
75
|
+
raise
|
76
|
+
except Exception as e:
|
77
|
+
logger.error(
|
78
|
+
"File move failed for artifact %s: %s",
|
79
|
+
artifact_id,
|
80
|
+
str(e),
|
81
|
+
extra={
|
82
|
+
"artifact_id": artifact_id,
|
83
|
+
"new_file_name": new_filename, # FIXED: Renamed from 'new_filename'
|
84
|
+
"new_session_id": new_session_id,
|
85
|
+
"operation": "move_file"
|
86
|
+
}
|
87
|
+
)
|
88
|
+
raise ProviderError(f"Move operation failed: {e}") from e
|
89
|
+
|
90
|
+
async def copy_file(
|
91
|
+
self,
|
92
|
+
artifact_id: str,
|
93
|
+
*,
|
94
|
+
new_filename: str = None,
|
95
|
+
target_session_id: str = None,
|
96
|
+
new_meta: Dict[str, Any] = None,
|
97
|
+
summary: str = None
|
98
|
+
) -> str:
|
99
|
+
"""
|
100
|
+
Copy a file WITHIN THE SAME SESSION only.
|
101
|
+
"""
|
102
|
+
self._check_closed()
|
103
|
+
|
104
|
+
try:
|
105
|
+
# Get original metadata first to check session
|
106
|
+
original_meta = await self._get_record(artifact_id)
|
107
|
+
original_session = original_meta.get("session_id")
|
108
|
+
|
109
|
+
# STRICT SECURITY: Block ALL cross-session copies
|
110
|
+
if target_session_id and target_session_id != original_session:
|
111
|
+
raise ArtifactStoreError(
|
112
|
+
f"Cross-session copies are not permitted for security reasons. "
|
113
|
+
f"Artifact {artifact_id} belongs to session '{original_session}', "
|
114
|
+
f"cannot copy to session '{target_session_id}'. Files can only be "
|
115
|
+
f"copied within the same session."
|
116
|
+
)
|
117
|
+
|
118
|
+
# Ensure target session is the same as source
|
119
|
+
copy_session = original_session # Always use source session
|
120
|
+
|
121
|
+
# Get original data
|
122
|
+
original_data = await self._retrieve_data(artifact_id)
|
123
|
+
|
124
|
+
# Prepare copy metadata
|
125
|
+
copy_filename = new_filename or (
|
126
|
+
(original_meta.get("filename", "file") or "file") + "_copy"
|
127
|
+
)
|
128
|
+
copy_summary = summary or f"Copy of {original_meta.get('summary', 'artifact')}"
|
129
|
+
|
130
|
+
# Merge metadata
|
131
|
+
copy_meta = {**original_meta.get("meta", {})}
|
132
|
+
if new_meta:
|
133
|
+
copy_meta.update(new_meta)
|
134
|
+
|
135
|
+
# Add copy tracking
|
136
|
+
copy_meta["copied_from"] = artifact_id
|
137
|
+
copy_meta["copy_timestamp"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
|
138
|
+
copy_meta["copy_within_session"] = original_session
|
139
|
+
|
140
|
+
# Store the copy using core operations
|
141
|
+
from .core import CoreStorageOperations
|
142
|
+
core_ops = CoreStorageOperations(self._artifact_store)
|
143
|
+
|
144
|
+
new_artifact_id = await core_ops.store(
|
145
|
+
data=original_data,
|
146
|
+
mime=original_meta["mime"],
|
147
|
+
summary=copy_summary,
|
148
|
+
filename=copy_filename,
|
149
|
+
session_id=copy_session, # Always same session
|
150
|
+
meta=copy_meta
|
151
|
+
)
|
152
|
+
|
153
|
+
logger.info(
|
154
|
+
"File copied within session: %s -> %s",
|
155
|
+
artifact_id,
|
156
|
+
new_artifact_id,
|
157
|
+
extra={
|
158
|
+
"source_artifact_id": artifact_id,
|
159
|
+
"new_artifact_id": new_artifact_id,
|
160
|
+
"session": copy_session,
|
161
|
+
"security_level": "same_session_only",
|
162
|
+
"operation": "copy_file"
|
163
|
+
}
|
164
|
+
)
|
165
|
+
|
166
|
+
return new_artifact_id
|
167
|
+
|
168
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
169
|
+
raise
|
170
|
+
except Exception as e:
|
171
|
+
logger.error(
|
172
|
+
"File copy failed for artifact %s: %s",
|
173
|
+
artifact_id,
|
174
|
+
str(e),
|
175
|
+
extra={
|
176
|
+
"artifact_id": artifact_id,
|
177
|
+
"new_file_name": new_filename, # FIXED: Renamed from 'new_filename'
|
178
|
+
"target_session_id": target_session_id,
|
179
|
+
"operation": "copy_file"
|
180
|
+
}
|
181
|
+
)
|
182
|
+
raise ProviderError(f"Copy operation failed: {e}") from e
|
183
|
+
|
184
|
+
async def read_file(
|
185
|
+
self,
|
186
|
+
artifact_id: str,
|
187
|
+
*,
|
188
|
+
encoding: str = "utf-8",
|
189
|
+
as_text: bool = True
|
190
|
+
) -> Union[str, bytes]:
|
191
|
+
"""
|
192
|
+
Read file content directly.
|
193
|
+
"""
|
194
|
+
self._check_closed()
|
195
|
+
|
196
|
+
try:
|
197
|
+
data = await self._retrieve_data(artifact_id)
|
198
|
+
|
199
|
+
if as_text:
|
200
|
+
try:
|
201
|
+
return data.decode(encoding)
|
202
|
+
except UnicodeDecodeError as e:
|
203
|
+
logger.warning(f"Failed to decode with {encoding}: {e}")
|
204
|
+
raise ProviderError(f"Cannot decode file as text with {encoding} encoding") from e
|
205
|
+
else:
|
206
|
+
return data
|
207
|
+
|
208
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
209
|
+
raise
|
210
|
+
except Exception as e:
|
211
|
+
logger.error(
|
212
|
+
"File read failed for artifact %s: %s",
|
213
|
+
artifact_id,
|
214
|
+
str(e),
|
215
|
+
extra={"artifact_id": artifact_id, "operation": "read_file"}
|
216
|
+
)
|
217
|
+
raise ProviderError(f"Read operation failed: {e}") from e
|
218
|
+
|
219
|
+
async def write_file(
|
220
|
+
self,
|
221
|
+
content: Union[str, bytes],
|
222
|
+
*,
|
223
|
+
filename: str,
|
224
|
+
mime: str = "text/plain",
|
225
|
+
summary: str = "",
|
226
|
+
session_id: str = None,
|
227
|
+
meta: Dict[str, Any] = None,
|
228
|
+
encoding: str = "utf-8",
|
229
|
+
overwrite_artifact_id: str = None
|
230
|
+
) -> str:
|
231
|
+
"""
|
232
|
+
Write content to a new file or overwrite existing WITHIN THE SAME SESSION.
|
233
|
+
"""
|
234
|
+
self._check_closed()
|
235
|
+
|
236
|
+
try:
|
237
|
+
# Convert content to bytes if needed
|
238
|
+
if isinstance(content, str):
|
239
|
+
data = content.encode(encoding)
|
240
|
+
else:
|
241
|
+
data = content
|
242
|
+
|
243
|
+
# Handle overwrite case with session security check
|
244
|
+
if overwrite_artifact_id:
|
245
|
+
try:
|
246
|
+
existing_meta = await self._get_record(overwrite_artifact_id)
|
247
|
+
existing_session = existing_meta.get("session_id")
|
248
|
+
|
249
|
+
# STRICT SECURITY: Can only overwrite files in the same session
|
250
|
+
if session_id and session_id != existing_session:
|
251
|
+
raise ArtifactStoreError(
|
252
|
+
f"Cross-session overwrite not permitted. Artifact {overwrite_artifact_id} "
|
253
|
+
f"belongs to session '{existing_session}', cannot overwrite from "
|
254
|
+
f"session '{session_id}'. Overwrite operations must be within the same session."
|
255
|
+
)
|
256
|
+
|
257
|
+
# Use the existing session if no session_id provided
|
258
|
+
session_id = session_id or existing_session
|
259
|
+
|
260
|
+
# Delete old version (within same session)
|
261
|
+
from .metadata import MetadataOperations
|
262
|
+
metadata_ops = MetadataOperations(self._artifact_store)
|
263
|
+
await metadata_ops.delete(overwrite_artifact_id)
|
264
|
+
|
265
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
266
|
+
pass # Original doesn't exist, proceed with new creation
|
267
|
+
|
268
|
+
# Store new content using core operations
|
269
|
+
from .core import CoreStorageOperations
|
270
|
+
core_ops = CoreStorageOperations(self._artifact_store)
|
271
|
+
|
272
|
+
write_meta = {**(meta or {})}
|
273
|
+
if overwrite_artifact_id:
|
274
|
+
write_meta["overwrote"] = overwrite_artifact_id
|
275
|
+
write_meta["overwrite_timestamp"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
|
276
|
+
write_meta["overwrite_within_session"] = session_id
|
277
|
+
|
278
|
+
artifact_id = await core_ops.store(
|
279
|
+
data=data,
|
280
|
+
mime=mime,
|
281
|
+
summary=summary or f"Written file: {filename}",
|
282
|
+
filename=filename,
|
283
|
+
session_id=session_id,
|
284
|
+
meta=write_meta
|
285
|
+
)
|
286
|
+
|
287
|
+
# FIXED: Use separate variables for logging to avoid 'filename' conflict
|
288
|
+
logger.info(
|
289
|
+
"File written successfully: %s (artifact_id: %s)",
|
290
|
+
filename,
|
291
|
+
artifact_id,
|
292
|
+
extra={
|
293
|
+
"artifact_id": artifact_id,
|
294
|
+
"file_name": filename, # FIXED: Renamed from 'filename'
|
295
|
+
"bytes": len(data),
|
296
|
+
"overwrite": bool(overwrite_artifact_id),
|
297
|
+
"session_id": session_id,
|
298
|
+
"security_level": "session_isolated",
|
299
|
+
"operation": "write_file"
|
300
|
+
}
|
301
|
+
)
|
302
|
+
|
303
|
+
return artifact_id
|
304
|
+
|
305
|
+
except Exception as e:
|
306
|
+
# FIXED: Use separate variables for logging to avoid 'filename' conflict
|
307
|
+
logger.error(
|
308
|
+
"File write failed for %s: %s",
|
309
|
+
filename,
|
310
|
+
str(e),
|
311
|
+
extra={
|
312
|
+
"file_name": filename, # FIXED: Renamed from 'filename'
|
313
|
+
"overwrite_artifact_id": overwrite_artifact_id,
|
314
|
+
"session_id": session_id,
|
315
|
+
"operation": "write_file"
|
316
|
+
}
|
317
|
+
)
|
318
|
+
raise ProviderError(f"Write operation failed: {e}") from e
|
319
|
+
|
320
|
+
async def get_directory_contents(
|
321
|
+
self,
|
322
|
+
session_id: str,
|
323
|
+
directory_prefix: str = "",
|
324
|
+
limit: int = 100
|
325
|
+
) -> List[Dict[str, Any]]:
|
326
|
+
"""
|
327
|
+
List files in a directory-like structure within a session.
|
328
|
+
"""
|
329
|
+
try:
|
330
|
+
from .metadata import MetadataOperations
|
331
|
+
metadata_ops = MetadataOperations(self._artifact_store)
|
332
|
+
return await metadata_ops.list_by_prefix(session_id, directory_prefix, limit)
|
333
|
+
except Exception as e:
|
334
|
+
logger.error(
|
335
|
+
"Directory listing failed for session %s: %s",
|
336
|
+
session_id,
|
337
|
+
str(e),
|
338
|
+
extra={
|
339
|
+
"session_id": session_id,
|
340
|
+
"directory_prefix": directory_prefix,
|
341
|
+
"operation": "get_directory_contents"
|
342
|
+
}
|
343
|
+
)
|
344
|
+
raise ProviderError(f"Directory listing failed: {e}") from e
|
345
|
+
|
346
|
+
async def _retrieve_data(self, artifact_id: str) -> bytes:
|
347
|
+
"""Helper to retrieve artifact data using core operations."""
|
348
|
+
from .core import CoreStorageOperations
|
349
|
+
core_ops = CoreStorageOperations(self._artifact_store)
|
350
|
+
return await core_ops.retrieve(artifact_id)
|
351
|
+
|
352
|
+
# Session security validation helper
|
353
|
+
async def _validate_session_access(self, artifact_id: str, expected_session_id: str = None) -> Dict[str, Any]:
|
354
|
+
"""
|
355
|
+
Validate that an artifact belongs to the expected session.
|
356
|
+
"""
|
357
|
+
record = await self._get_record(artifact_id)
|
358
|
+
actual_session = record.get("session_id")
|
359
|
+
|
360
|
+
if expected_session_id and actual_session != expected_session_id:
|
361
|
+
raise ArtifactStoreError(
|
362
|
+
f"Session access violation: Artifact {artifact_id} belongs to "
|
363
|
+
f"session '{actual_session}', but access was attempted from "
|
364
|
+
f"session '{expected_session_id}'. Cross-session access is not permitted."
|
365
|
+
)
|
366
|
+
|
367
|
+
return record
|
chuk_artifacts/store.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
|
-
# chuk_artifacts/store.py
|
2
|
+
# chuk_artifacts/store.py (ENHANCED)
|
3
3
|
"""
|
4
|
-
Asynchronous, object-store-backed artefact manager with
|
4
|
+
Asynchronous, object-store-backed artefact manager with MCP server support.
|
5
5
|
"""
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
9
|
import os, logging
|
10
|
-
from typing import Any, Dict, List, Callable, AsyncContextManager, Optional
|
10
|
+
from typing import Any, Dict, List, Callable, AsyncContextManager, Optional, Union
|
11
11
|
|
12
12
|
try:
|
13
13
|
import aioboto3
|
@@ -51,15 +51,9 @@ def _default_session_factory() -> Callable[[], AsyncContextManager]:
|
|
51
51
|
# ─────────────────────────────────────────────────────────────────────
|
52
52
|
class ArtifactStore:
|
53
53
|
"""
|
54
|
-
|
54
|
+
Asynchronous artifact storage with MCP server support.
|
55
55
|
|
56
|
-
|
57
|
-
Now properly delegates operations to specialized modules:
|
58
|
-
- CoreStorageOperations: store() and retrieve()
|
59
|
-
- PresignedURLOperations: presign*() methods
|
60
|
-
- MetadataOperations: metadata(), exists(), delete()
|
61
|
-
- BatchOperations: store_batch()
|
62
|
-
- AdminOperations: validate_configuration(), get_stats()
|
56
|
+
Enhanced with MCP-specific operations for file management within sessions.
|
63
57
|
"""
|
64
58
|
|
65
59
|
def __init__(
|
@@ -129,22 +123,23 @@ class ArtifactStore:
|
|
129
123
|
self._session_provider_name = session_provider or "memory"
|
130
124
|
self._closed = False
|
131
125
|
|
132
|
-
# Initialize operation modules
|
133
|
-
# FIXED: Now works correctly with the fixed BaseOperations
|
126
|
+
# Initialize operation modules
|
134
127
|
from .core import CoreStorageOperations
|
135
128
|
from .presigned import PresignedURLOperations
|
136
129
|
from .metadata import MetadataOperations
|
137
130
|
from .batch import BatchOperations
|
138
131
|
from .admin import AdminOperations
|
132
|
+
from .session_operations import SessionOperations
|
139
133
|
|
140
134
|
self._core = CoreStorageOperations(self)
|
141
135
|
self._presigned = PresignedURLOperations(self)
|
142
136
|
self._metadata = MetadataOperations(self)
|
143
137
|
self._batch = BatchOperations(self)
|
144
138
|
self._admin = AdminOperations(self)
|
139
|
+
self._session = SessionOperations(self)
|
145
140
|
|
146
141
|
logger.info(
|
147
|
-
"ArtifactStore initialized with
|
142
|
+
"ArtifactStore initialized with session operations support",
|
148
143
|
extra={
|
149
144
|
"bucket": bucket,
|
150
145
|
"storage_provider": self._storage_provider_name,
|
@@ -284,15 +279,20 @@ class ArtifactStore:
|
|
284
279
|
summary: str = None,
|
285
280
|
meta: Dict[str, Any] = None,
|
286
281
|
filename: str = None,
|
287
|
-
ttl: int = None
|
282
|
+
ttl: int = None,
|
283
|
+
# NEW: MCP-specific parameters
|
284
|
+
new_meta: Dict[str, Any] = None,
|
285
|
+
merge: bool = True
|
288
286
|
) -> Dict[str, Any]:
|
289
|
-
"""Update artifact metadata
|
287
|
+
"""Update artifact metadata with MCP server compatibility."""
|
290
288
|
return await self._metadata.update_metadata(
|
291
289
|
artifact_id,
|
292
290
|
summary=summary,
|
293
291
|
meta=meta,
|
294
292
|
filename=filename,
|
295
|
-
ttl=ttl
|
293
|
+
ttl=ttl,
|
294
|
+
new_meta=new_meta,
|
295
|
+
merge=merge
|
296
296
|
)
|
297
297
|
|
298
298
|
async def extend_ttl(self, artifact_id: str, additional_seconds: int) -> Dict[str, Any]:
|
@@ -303,6 +303,15 @@ class ArtifactStore:
|
|
303
303
|
"""List artifacts for a specific session."""
|
304
304
|
return await self._metadata.list_by_session(session_id, limit)
|
305
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
|
+
|
306
315
|
# ─────────────────────────────────────────────────────────────────
|
307
316
|
# Batch operations (delegated to BatchOperations)
|
308
317
|
# ─────────────────────────────────────────────────────────────────
|
@@ -328,6 +337,95 @@ class ArtifactStore:
|
|
328
337
|
"""Get storage statistics."""
|
329
338
|
return await self._admin.get_stats()
|
330
339
|
|
340
|
+
# ─────────────────────────────────────────────────────────────────
|
341
|
+
# Session-based file operations (delegated to SessionOperations)
|
342
|
+
# ─────────────────────────────────────────────────────────────────
|
343
|
+
|
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
|
+
)
|
391
|
+
|
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
|
+
)
|
415
|
+
|
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
|
+
)
|
428
|
+
|
331
429
|
# ─────────────────────────────────────────────────────────────────
|
332
430
|
# Resource management
|
333
431
|
# ─────────────────────────────────────────────────────────────────
|