chuk-artifacts 0.1.3__py3-none-any.whl → 0.1.5__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,366 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # chuk_artifacts/session/session_operations.py
3
- """
4
- Session-based file operations with strict session isolation.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import uuid, hashlib, json, logging
10
- from datetime import datetime
11
- from typing import Any, Dict, Optional, Union, List
12
-
13
- from ..base import BaseOperations
14
- from ..exceptions import (
15
- ArtifactStoreError, ArtifactNotFoundError, ArtifactExpiredError,
16
- ProviderError, SessionError
17
- )
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
- _ANON_PREFIX = "anon"
22
- _DEFAULT_TTL = 900
23
-
24
-
25
- class SessionOperations(BaseOperations):
26
- """Session-based file operations with strict session isolation."""
27
-
28
- async def move_file(
29
- self,
30
- artifact_id: str,
31
- *,
32
- new_filename: str = None,
33
- new_session_id: str = None,
34
- new_meta: Dict[str, Any] = None
35
- ) -> Dict[str, Any]:
36
- """
37
- Move a file within the SAME session or rename it.
38
- """
39
- self._check_closed()
40
-
41
- try:
42
- # Get current metadata
43
- record = await self._get_record(artifact_id)
44
- current_session = record.get("session_id")
45
-
46
- # STRICT SECURITY: Block ALL cross-session moves
47
- if new_session_id and new_session_id != current_session:
48
- raise ArtifactStoreError(
49
- f"Cross-session moves are not permitted for security reasons. "
50
- f"Artifact {artifact_id} belongs to session '{current_session}', "
51
- f"cannot move to session '{new_session_id}'. Use copy operations within "
52
- f"the same session only."
53
- )
54
-
55
- # Update metadata fields (only filename and meta allowed)
56
- updates = {}
57
- if new_filename:
58
- updates["filename"] = new_filename
59
- if new_meta:
60
- existing_meta = record.get("meta", {})
61
- existing_meta.update(new_meta)
62
- updates["new_meta"] = existing_meta
63
- updates["merge"] = True
64
-
65
- if updates:
66
- # Use the metadata operations to update
67
- from ..metadata import MetadataOperations
68
- metadata_ops = MetadataOperations(self._artifact_store)
69
- return await metadata_ops.update_metadata(artifact_id, **updates)
70
-
71
- return record
72
-
73
- except (ArtifactNotFoundError, ArtifactExpiredError):
74
- raise
75
- except Exception as e:
76
- logger.error(
77
- "File move failed for artifact %s: %s",
78
- artifact_id,
79
- str(e),
80
- extra={
81
- "artifact_id": artifact_id,
82
- "new_file_name": new_filename, # FIXED: Renamed from 'new_filename'
83
- "new_session_id": new_session_id,
84
- "operation": "move_file"
85
- }
86
- )
87
- raise ProviderError(f"Move operation failed: {e}") from e
88
-
89
- async def copy_file(
90
- self,
91
- artifact_id: str,
92
- *,
93
- new_filename: str = None,
94
- target_session_id: str = None,
95
- new_meta: Dict[str, Any] = None,
96
- summary: str = None
97
- ) -> str:
98
- """
99
- Copy a file WITHIN THE SAME SESSION only.
100
- """
101
- self._check_closed()
102
-
103
- try:
104
- # Get original metadata first to check session
105
- original_meta = await self._get_record(artifact_id)
106
- original_session = original_meta.get("session_id")
107
-
108
- # STRICT SECURITY: Block ALL cross-session copies
109
- if target_session_id and target_session_id != original_session:
110
- raise ArtifactStoreError(
111
- f"Cross-session copies are not permitted for security reasons. "
112
- f"Artifact {artifact_id} belongs to session '{original_session}', "
113
- f"cannot copy to session '{target_session_id}'. Files can only be "
114
- f"copied within the same session."
115
- )
116
-
117
- # Ensure target session is the same as source
118
- copy_session = original_session # Always use source session
119
-
120
- # Get original data
121
- original_data = await self._retrieve_data(artifact_id)
122
-
123
- # Prepare copy metadata
124
- copy_filename = new_filename or (
125
- (original_meta.get("filename", "file") or "file") + "_copy"
126
- )
127
- copy_summary = summary or f"Copy of {original_meta.get('summary', 'artifact')}"
128
-
129
- # Merge metadata
130
- copy_meta = {**original_meta.get("meta", {})}
131
- if new_meta:
132
- copy_meta.update(new_meta)
133
-
134
- # Add copy tracking
135
- copy_meta["copied_from"] = artifact_id
136
- copy_meta["copy_timestamp"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
137
- copy_meta["copy_within_session"] = original_session
138
-
139
- # Store the copy using core operations
140
- from ..core import CoreStorageOperations
141
- core_ops = CoreStorageOperations(self._artifact_store)
142
-
143
- new_artifact_id = await core_ops.store(
144
- data=original_data,
145
- mime=original_meta["mime"],
146
- summary=copy_summary,
147
- filename=copy_filename,
148
- session_id=copy_session, # Always same session
149
- meta=copy_meta
150
- )
151
-
152
- logger.info(
153
- "File copied within session: %s -> %s",
154
- artifact_id,
155
- new_artifact_id,
156
- extra={
157
- "source_artifact_id": artifact_id,
158
- "new_artifact_id": new_artifact_id,
159
- "session": copy_session,
160
- "security_level": "same_session_only",
161
- "operation": "copy_file"
162
- }
163
- )
164
-
165
- return new_artifact_id
166
-
167
- except (ArtifactNotFoundError, ArtifactExpiredError):
168
- raise
169
- except Exception as e:
170
- logger.error(
171
- "File copy failed for artifact %s: %s",
172
- artifact_id,
173
- str(e),
174
- extra={
175
- "artifact_id": artifact_id,
176
- "new_file_name": new_filename, # FIXED: Renamed from 'new_filename'
177
- "target_session_id": target_session_id,
178
- "operation": "copy_file"
179
- }
180
- )
181
- raise ProviderError(f"Copy operation failed: {e}") from e
182
-
183
- async def read_file(
184
- self,
185
- artifact_id: str,
186
- *,
187
- encoding: str = "utf-8",
188
- as_text: bool = True
189
- ) -> Union[str, bytes]:
190
- """
191
- Read file content directly.
192
- """
193
- self._check_closed()
194
-
195
- try:
196
- data = await self._retrieve_data(artifact_id)
197
-
198
- if as_text:
199
- try:
200
- return data.decode(encoding)
201
- except UnicodeDecodeError as e:
202
- logger.warning(f"Failed to decode with {encoding}: {e}")
203
- raise ProviderError(f"Cannot decode file as text with {encoding} encoding") from e
204
- else:
205
- return data
206
-
207
- except (ArtifactNotFoundError, ArtifactExpiredError):
208
- raise
209
- except Exception as e:
210
- logger.error(
211
- "File read failed for artifact %s: %s",
212
- artifact_id,
213
- str(e),
214
- extra={"artifact_id": artifact_id, "operation": "read_file"}
215
- )
216
- raise ProviderError(f"Read operation failed: {e}") from e
217
-
218
- async def write_file(
219
- self,
220
- content: Union[str, bytes],
221
- *,
222
- filename: str,
223
- mime: str = "text/plain",
224
- summary: str = "",
225
- session_id: str = None,
226
- meta: Dict[str, Any] = None,
227
- encoding: str = "utf-8",
228
- overwrite_artifact_id: str = None
229
- ) -> str:
230
- """
231
- Write content to a new file or overwrite existing WITHIN THE SAME SESSION.
232
- """
233
- self._check_closed()
234
-
235
- try:
236
- # Convert content to bytes if needed
237
- if isinstance(content, str):
238
- data = content.encode(encoding)
239
- else:
240
- data = content
241
-
242
- # Handle overwrite case with session security check
243
- if overwrite_artifact_id:
244
- try:
245
- existing_meta = await self._get_record(overwrite_artifact_id)
246
- existing_session = existing_meta.get("session_id")
247
-
248
- # STRICT SECURITY: Can only overwrite files in the same session
249
- if session_id and session_id != existing_session:
250
- raise ArtifactStoreError(
251
- f"Cross-session overwrite not permitted. Artifact {overwrite_artifact_id} "
252
- f"belongs to session '{existing_session}', cannot overwrite from "
253
- f"session '{session_id}'. Overwrite operations must be within the same session."
254
- )
255
-
256
- # Use the existing session if no session_id provided
257
- session_id = session_id or existing_session
258
-
259
- # Delete old version (within same session)
260
- from ..metadata import MetadataOperations
261
- metadata_ops = MetadataOperations(self._artifact_store)
262
- await metadata_ops.delete(overwrite_artifact_id)
263
-
264
- except (ArtifactNotFoundError, ArtifactExpiredError):
265
- pass # Original doesn't exist, proceed with new creation
266
-
267
- # Store new content using core operations
268
- from ..core import CoreStorageOperations
269
- core_ops = CoreStorageOperations(self._artifact_store)
270
-
271
- write_meta = {**(meta or {})}
272
- if overwrite_artifact_id:
273
- write_meta["overwrote"] = overwrite_artifact_id
274
- write_meta["overwrite_timestamp"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
275
- write_meta["overwrite_within_session"] = session_id
276
-
277
- artifact_id = await core_ops.store(
278
- data=data,
279
- mime=mime,
280
- summary=summary or f"Written file: {filename}",
281
- filename=filename,
282
- session_id=session_id,
283
- meta=write_meta
284
- )
285
-
286
- # FIXED: Use separate variables for logging to avoid 'filename' conflict
287
- logger.info(
288
- "File written successfully: %s (artifact_id: %s)",
289
- filename,
290
- artifact_id,
291
- extra={
292
- "artifact_id": artifact_id,
293
- "file_name": filename, # FIXED: Renamed from 'filename'
294
- "bytes": len(data),
295
- "overwrite": bool(overwrite_artifact_id),
296
- "session_id": session_id,
297
- "security_level": "session_isolated",
298
- "operation": "write_file"
299
- }
300
- )
301
-
302
- return artifact_id
303
-
304
- except Exception as e:
305
- # FIXED: Use separate variables for logging to avoid 'filename' conflict
306
- logger.error(
307
- "File write failed for %s: %s",
308
- filename,
309
- str(e),
310
- extra={
311
- "file_name": filename, # FIXED: Renamed from 'filename'
312
- "overwrite_artifact_id": overwrite_artifact_id,
313
- "session_id": session_id,
314
- "operation": "write_file"
315
- }
316
- )
317
- raise ProviderError(f"Write operation failed: {e}") from e
318
-
319
- async def get_directory_contents(
320
- self,
321
- session_id: str,
322
- directory_prefix: str = "",
323
- limit: int = 100
324
- ) -> List[Dict[str, Any]]:
325
- """
326
- List files in a directory-like structure within a session.
327
- """
328
- try:
329
- from ..metadata import MetadataOperations
330
- metadata_ops = MetadataOperations(self._artifact_store)
331
- return await metadata_ops.list_by_prefix(session_id, directory_prefix, limit)
332
- except Exception as e:
333
- logger.error(
334
- "Directory listing failed for session %s: %s",
335
- session_id,
336
- str(e),
337
- extra={
338
- "session_id": session_id,
339
- "directory_prefix": directory_prefix,
340
- "operation": "get_directory_contents"
341
- }
342
- )
343
- raise ProviderError(f"Directory listing failed: {e}") from e
344
-
345
- async def _retrieve_data(self, artifact_id: str) -> bytes:
346
- """Helper to retrieve artifact data using core operations."""
347
- from ..core import CoreStorageOperations
348
- core_ops = CoreStorageOperations(self._artifact_store)
349
- return await core_ops.retrieve(artifact_id)
350
-
351
- # Session security validation helper
352
- async def _validate_session_access(self, artifact_id: str, expected_session_id: str = None) -> Dict[str, Any]:
353
- """
354
- Validate that an artifact belongs to the expected session.
355
- """
356
- record = await self._get_record(artifact_id)
357
- actual_session = record.get("session_id")
358
-
359
- if expected_session_id and actual_session != expected_session_id:
360
- raise ArtifactStoreError(
361
- f"Session access violation: Artifact {artifact_id} belongs to "
362
- f"session '{actual_session}', but access was attempted from "
363
- f"session '{expected_session_id}'. Cross-session access is not permitted."
364
- )
365
-
366
- return record
@@ -1,26 +0,0 @@
1
- chuk_artifacts/__init__.py,sha256=-4S9FWKVcQSa2ZD3GVbmbpGZPcl0cTQN_TFZLSqV7lQ,3605
2
- chuk_artifacts/admin.py,sha256=9wF-xuxLpKcjJLgZS0rDSeXOAjj4lC9R2VtHpzBuKIA,2931
3
- chuk_artifacts/base.py,sha256=d3iA3AJR9wF0I3kmppkosPDxaMTgufqI6v_wW5fQfKY,2411
4
- chuk_artifacts/batch.py,sha256=I5WgWajuzrvqTCkQYYgpzrL1WduzxJchA3Ihv3Xvhcw,4264
5
- chuk_artifacts/config.py,sha256=MaUzHzKPoBUyERviEpv8JVvPybMzSksgLyj0b7AO3Sc,7664
6
- chuk_artifacts/core.py,sha256=V9s5YZt9UTA5Kj-1btK5dCB2o_1zeteK0Mo9VaPv4oQ,6488
7
- chuk_artifacts/exceptions.py,sha256=f-s7Mg7c8vMXsbgqO2B6lMHdXcJQNvsESAY4GhJaV4g,814
8
- chuk_artifacts/metadata.py,sha256=n_0oQg3DdZG03pmTutHlN2o0os-IJN2Fhwer1_qtY5s,7382
9
- chuk_artifacts/models.py,sha256=_foXlkr0DprqgztDw5WtlDc-s1OouLgYNp4XM1Ghp-g,837
10
- chuk_artifacts/presigned.py,sha256=9wGl403HxpfAXcUtkhCSjVCYJftv9uOpyfTs6gQhSZg,11215
11
- chuk_artifacts/provider_factory.py,sha256=T0IXx1C8gygJzp417oB44_DxEaZoZR7jcdwQy8FghRE,3398
12
- chuk_artifacts/store.py,sha256=S6sRDLUaHJmzPE4j642wrLFBKR68Wp7y4WWGI7B92tw,23290
13
- chuk_artifacts/providers/__init__.py,sha256=3lN1lAy1ETT1mQslJo1f22PPR1W4CyxmsqJBclzH4NE,317
14
- chuk_artifacts/providers/filesystem.py,sha256=F4EjE-_ItPg0RWe7CqameVpOMjU-b7AigEBkm_ZoNrc,15280
15
- chuk_artifacts/providers/ibm_cos.py,sha256=K1-VAX4UVV9tA161MOeDXOKloQ0hB77jdw1-p46FwmU,4445
16
- chuk_artifacts/providers/ibm_cos_iam.py,sha256=VtwvCi9rMMcZx6i9l21ob6wM8jXseqvjzgCnAA82RkY,3186
17
- chuk_artifacts/providers/memory.py,sha256=B1C-tR1PcNz-UuDfGm1bhjPz3oITVATIMPekVbE7nm4,10487
18
- chuk_artifacts/providers/s3.py,sha256=eWhBhFSaobpRbazn7ySfU_7D8rm_xCfdSVqRtzXzXRY,2858
19
- chuk_artifacts/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- chuk_artifacts/session/session_manager.py,sha256=B-_2AfFM22Vu0ZefF2KJ6vVGPU7naP87Uv4H9sW7oqA,6873
21
- chuk_artifacts/session/session_operations.py,sha256=9BBXeYfz1AE_QS0iunK7aJH-rUC9a33GF_2DYbBJtdI,14174
22
- chuk_artifacts-0.1.3.dist-info/licenses/LICENSE,sha256=SG9BmgtPBagPV0d-Fep-msdAGl-E1CeoBL7-EDRH2qA,1066
23
- chuk_artifacts-0.1.3.dist-info/METADATA,sha256=e2qc0fXl9Y7oJvzBXaFAdUBg4qFNBlYe635vUiybw5U,21188
24
- chuk_artifacts-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- chuk_artifacts-0.1.3.dist-info/top_level.txt,sha256=1_PVMtWXR0A-ZmeH6apF9mPaMtU0i23JE6wmN4GBRDI,15
26
- chuk_artifacts-0.1.3.dist-info/RECORD,,