chuk-artifacts 0.1.2__py3-none-any.whl → 0.1.4__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,367 +0,0 @@
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