chuk-artifacts 0.3__py3-none-any.whl → 0.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.
- chuk_artifacts/grid.py +140 -7
- chuk_artifacts/metadata.py +1 -0
- chuk_artifacts/models.py +3 -3
- chuk_artifacts/providers/filesystem.py +23 -3
- chuk_artifacts/providers/ibm_cos.py +61 -59
- chuk_artifacts/providers/ibm_cos_iam.py +51 -7
- chuk_artifacts/providers/memory.py +32 -4
- chuk_artifacts/store.py +47 -1
- chuk_artifacts-0.4.dist-info/METADATA +730 -0
- {chuk_artifacts-0.3.dist-info → chuk_artifacts-0.4.dist-info}/RECORD +12 -12
- chuk_artifacts-0.3.dist-info/METADATA +0 -719
- {chuk_artifacts-0.3.dist-info → chuk_artifacts-0.4.dist-info}/WHEEL +0 -0
- {chuk_artifacts-0.3.dist-info → chuk_artifacts-0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,730 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: chuk-artifacts
|
3
|
+
Version: 0.4
|
4
|
+
Summary: Chuk Artifacts provides a production-ready, modular artifact storage system that works seamlessly across multiple storage backends (memory, filesystem, AWS S3, IBM Cloud Object Storage) with Redis or memory-based metadata caching and strict session-based security.
|
5
|
+
Requires-Python: >=3.11
|
6
|
+
Description-Content-Type: text/markdown
|
7
|
+
Requires-Dist: pydantic>=2.10.6
|
8
|
+
Requires-Dist: pyyaml>=6.0.2
|
9
|
+
Requires-Dist: aioboto3>=14.3.0
|
10
|
+
Requires-Dist: redis>=6.2.0
|
11
|
+
Requires-Dist: ibm-cos-sdk>=2.13.5
|
12
|
+
Requires-Dist: chuk-sessions>=0.3
|
13
|
+
Requires-Dist: dotenv>=0.9.9
|
14
|
+
Requires-Dist: asyncio>=3.4.3
|
15
|
+
Provides-Extra: dev
|
16
|
+
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
17
|
+
Requires-Dist: ruff>=0.4.6; extra == "dev"
|
18
|
+
|
19
|
+
# Chuk Artifacts
|
20
|
+
|
21
|
+
> **Async artifact storage with session-based security and multi-backend support**
|
22
|
+
|
23
|
+
A production-ready Python library for storing and managing files across multiple storage backends (S3, IBM COS, filesystem, memory) with Redis-based metadata caching and strict session isolation.
|
24
|
+
|
25
|
+
[](https://python.org)
|
26
|
+
[](https://opensource.org/licenses/MIT)
|
27
|
+
[](https://docs.python.org/3/library/asyncio.html)
|
28
|
+
|
29
|
+
## Why Chuk Artifacts?
|
30
|
+
|
31
|
+
- 🔒 **Session-based security** - Every file belongs to a session, preventing data leaks
|
32
|
+
- 🌐 **Multiple backends** - Switch between S3, filesystem, memory without code changes
|
33
|
+
- ⚡ **High performance** - 3,000+ operations/second with async/await
|
34
|
+
- 🎯 **Zero config** - Works out of the box, configure only what you need
|
35
|
+
- 🔗 **Presigned URLs** - Secure file access without exposing credentials
|
36
|
+
- 📦 **Grid architecture** - Organized paths for scalability and federation
|
37
|
+
|
38
|
+
## Quick Start
|
39
|
+
|
40
|
+
### Installation
|
41
|
+
|
42
|
+
```bash
|
43
|
+
pip install chuk-artifacts
|
44
|
+
```
|
45
|
+
|
46
|
+
### 30-Second Example
|
47
|
+
|
48
|
+
```python
|
49
|
+
from chuk_artifacts import ArtifactStore
|
50
|
+
|
51
|
+
# Works immediately - no configuration needed
|
52
|
+
async with ArtifactStore() as store:
|
53
|
+
# Store a file
|
54
|
+
file_id = await store.store(
|
55
|
+
data=b"Hello, world!",
|
56
|
+
mime="text/plain",
|
57
|
+
summary="My first file",
|
58
|
+
filename="hello.txt"
|
59
|
+
)
|
60
|
+
|
61
|
+
# Get it back
|
62
|
+
content = await store.retrieve(file_id)
|
63
|
+
print(content.decode()) # "Hello, world!"
|
64
|
+
|
65
|
+
# Share with a secure URL
|
66
|
+
url = await store.presign(file_id)
|
67
|
+
print(f"Download: {url}")
|
68
|
+
```
|
69
|
+
|
70
|
+
That's it! No AWS credentials, no Redis setup, no configuration files. Perfect for development and testing.
|
71
|
+
|
72
|
+
## Core Concepts
|
73
|
+
|
74
|
+
### Sessions = Security Boundaries
|
75
|
+
|
76
|
+
Every file belongs to a **session**. Sessions prevent users from accessing each other's files:
|
77
|
+
|
78
|
+
```python
|
79
|
+
# Files are isolated by session
|
80
|
+
alice_file = await store.store(
|
81
|
+
data=b"Alice's private data",
|
82
|
+
mime="text/plain",
|
83
|
+
summary="Private file",
|
84
|
+
session_id="user_alice" # Alice's session
|
85
|
+
)
|
86
|
+
|
87
|
+
bob_file = await store.store(
|
88
|
+
data=b"Bob's private data",
|
89
|
+
mime="text/plain",
|
90
|
+
summary="Private file",
|
91
|
+
session_id="user_bob" # Bob's session
|
92
|
+
)
|
93
|
+
|
94
|
+
# Alice can't access Bob's files
|
95
|
+
alice_files = await store.list_by_session("user_alice") # Only Alice's files
|
96
|
+
bob_files = await store.list_by_session("user_bob") # Only Bob's files
|
97
|
+
|
98
|
+
# Cross-session operations are blocked
|
99
|
+
await store.copy_file(alice_file, target_session_id="user_bob") # ❌ Denied
|
100
|
+
```
|
101
|
+
|
102
|
+
### Grid Architecture
|
103
|
+
|
104
|
+
Files are organized in a predictable hierarchy:
|
105
|
+
```
|
106
|
+
grid/
|
107
|
+
├── sandbox_id/
|
108
|
+
│ ├── session_alice/
|
109
|
+
│ │ ├── file_1
|
110
|
+
│ │ └── file_2
|
111
|
+
│ └── session_bob/
|
112
|
+
│ ├── file_3
|
113
|
+
│ └── file_4
|
114
|
+
```
|
115
|
+
|
116
|
+
This makes the system **multi-tenant safe** and **federation-ready**.
|
117
|
+
|
118
|
+
## File Operations
|
119
|
+
|
120
|
+
### Basic Operations
|
121
|
+
|
122
|
+
```python
|
123
|
+
# Store a file
|
124
|
+
file_id = await store.store(
|
125
|
+
data=file_bytes,
|
126
|
+
mime="image/jpeg",
|
127
|
+
summary="Profile photo",
|
128
|
+
filename="avatar.jpg",
|
129
|
+
session_id="user_123"
|
130
|
+
)
|
131
|
+
|
132
|
+
# Read file content directly
|
133
|
+
content = await store.read_file(file_id, as_text=True)
|
134
|
+
|
135
|
+
# Write text files easily
|
136
|
+
doc_id = await store.write_file(
|
137
|
+
content="# My Document\n\nHello world!",
|
138
|
+
filename="docs/readme.md",
|
139
|
+
mime="text/markdown",
|
140
|
+
session_id="user_123"
|
141
|
+
)
|
142
|
+
|
143
|
+
# Check if file exists
|
144
|
+
if await store.exists(file_id):
|
145
|
+
print("File found!")
|
146
|
+
|
147
|
+
# Delete file
|
148
|
+
await store.delete(file_id)
|
149
|
+
```
|
150
|
+
|
151
|
+
### Directory-Like Operations
|
152
|
+
|
153
|
+
```python
|
154
|
+
# List files in a session
|
155
|
+
files = await store.list_by_session("user_123")
|
156
|
+
|
157
|
+
# List files in a "directory"
|
158
|
+
docs = await store.get_directory_contents("user_123", "docs/")
|
159
|
+
images = await store.get_directory_contents("user_123", "images/")
|
160
|
+
|
161
|
+
# Copy files (within same session only)
|
162
|
+
backup_id = await store.copy_file(
|
163
|
+
file_id,
|
164
|
+
new_filename="docs/readme_backup.md"
|
165
|
+
)
|
166
|
+
```
|
167
|
+
|
168
|
+
### Metadata and Updates
|
169
|
+
|
170
|
+
```python
|
171
|
+
# Get file metadata
|
172
|
+
meta = await store.metadata(file_id)
|
173
|
+
print(f"Size: {meta['bytes']} bytes")
|
174
|
+
print(f"Created: {meta['stored_at']}")
|
175
|
+
|
176
|
+
# Update metadata
|
177
|
+
await store.update_metadata(
|
178
|
+
file_id,
|
179
|
+
summary="Updated description",
|
180
|
+
meta={"version": 2, "author": "Alice"}
|
181
|
+
)
|
182
|
+
|
183
|
+
# Update file content
|
184
|
+
await store.update_file(
|
185
|
+
file_id,
|
186
|
+
data=b"New content",
|
187
|
+
summary="Updated file"
|
188
|
+
)
|
189
|
+
```
|
190
|
+
|
191
|
+
## Storage Providers
|
192
|
+
|
193
|
+
### Memory Provider (Default)
|
194
|
+
|
195
|
+
Perfect for development and testing:
|
196
|
+
|
197
|
+
```python
|
198
|
+
# Automatic - no configuration needed
|
199
|
+
store = ArtifactStore()
|
200
|
+
```
|
201
|
+
|
202
|
+
- ✅ Zero setup
|
203
|
+
- ✅ Fast
|
204
|
+
- ❌ Non-persistent (lost on restart)
|
205
|
+
|
206
|
+
### Filesystem Provider
|
207
|
+
|
208
|
+
Local disk storage:
|
209
|
+
|
210
|
+
```python
|
211
|
+
store = ArtifactStore(storage_provider="filesystem")
|
212
|
+
|
213
|
+
# Or via environment
|
214
|
+
export ARTIFACT_PROVIDER=filesystem
|
215
|
+
export ARTIFACT_FS_ROOT=./my-files
|
216
|
+
```
|
217
|
+
|
218
|
+
- ✅ Persistent
|
219
|
+
- ✅ Good for development
|
220
|
+
- ✅ Easy debugging
|
221
|
+
- ❌ Not suitable for production clustering
|
222
|
+
|
223
|
+
### AWS S3 Provider
|
224
|
+
|
225
|
+
Production-ready cloud storage:
|
226
|
+
|
227
|
+
```python
|
228
|
+
store = ArtifactStore(storage_provider="s3")
|
229
|
+
|
230
|
+
# Configure via environment
|
231
|
+
export ARTIFACT_PROVIDER=s3
|
232
|
+
export AWS_ACCESS_KEY_ID=your_key
|
233
|
+
export AWS_SECRET_ACCESS_KEY=your_secret
|
234
|
+
export AWS_REGION=us-east-1
|
235
|
+
export ARTIFACT_BUCKET=my-bucket
|
236
|
+
```
|
237
|
+
|
238
|
+
- ✅ Highly scalable
|
239
|
+
- ✅ Durable (99.999999999%)
|
240
|
+
- ✅ Native presigned URLs
|
241
|
+
- ✅ Production ready
|
242
|
+
|
243
|
+
### IBM Cloud Object Storage
|
244
|
+
|
245
|
+
Enterprise object storage:
|
246
|
+
|
247
|
+
```python
|
248
|
+
# HMAC credentials
|
249
|
+
store = ArtifactStore(storage_provider="ibm_cos")
|
250
|
+
|
251
|
+
export ARTIFACT_PROVIDER=ibm_cos
|
252
|
+
export AWS_ACCESS_KEY_ID=your_hmac_key
|
253
|
+
export AWS_SECRET_ACCESS_KEY=your_hmac_secret
|
254
|
+
export IBM_COS_ENDPOINT=https://s3.us-south.cloud-object-storage.appdomain.cloud
|
255
|
+
|
256
|
+
# Or IAM credentials
|
257
|
+
store = ArtifactStore(storage_provider="ibm_cos_iam")
|
258
|
+
|
259
|
+
export ARTIFACT_PROVIDER=ibm_cos_iam
|
260
|
+
export IBM_COS_APIKEY=your_api_key
|
261
|
+
export IBM_COS_INSTANCE_CRN=crn:v1:bluemix:public:cloud-object-storage:...
|
262
|
+
```
|
263
|
+
|
264
|
+
## Session Providers
|
265
|
+
|
266
|
+
### Memory Sessions (Default)
|
267
|
+
|
268
|
+
Fast, in-memory metadata storage:
|
269
|
+
|
270
|
+
```python
|
271
|
+
store = ArtifactStore(session_provider="memory")
|
272
|
+
```
|
273
|
+
|
274
|
+
- ✅ Fast
|
275
|
+
- ✅ No setup
|
276
|
+
- ❌ Non-persistent
|
277
|
+
- ❌ Single instance only
|
278
|
+
|
279
|
+
### Redis Sessions
|
280
|
+
|
281
|
+
Persistent, shared metadata storage:
|
282
|
+
|
283
|
+
```python
|
284
|
+
store = ArtifactStore(session_provider="redis")
|
285
|
+
|
286
|
+
# Configure via environment
|
287
|
+
export SESSION_PROVIDER=redis
|
288
|
+
export SESSION_REDIS_URL=redis://localhost:6379/0
|
289
|
+
```
|
290
|
+
|
291
|
+
- ✅ Persistent
|
292
|
+
- ✅ Shared across instances
|
293
|
+
- ✅ Production ready
|
294
|
+
- ✅ High performance
|
295
|
+
|
296
|
+
## Environment Variables
|
297
|
+
|
298
|
+
| Variable | Description | Default | Examples |
|
299
|
+
|----------|-------------|---------|----------|
|
300
|
+
| **Storage Configuration** |
|
301
|
+
| `ARTIFACT_PROVIDER` | Storage backend | `memory` | `s3`, `filesystem`, `ibm_cos` |
|
302
|
+
| `ARTIFACT_BUCKET` | Bucket/container name | `artifacts` | `my-files`, `prod-storage` |
|
303
|
+
| `ARTIFACT_FS_ROOT` | Filesystem root directory | `./artifacts` | `/data/files`, `~/storage` |
|
304
|
+
| `ARTIFACT_SANDBOX_ID` | Sandbox identifier | Auto-generated | `myapp`, `prod-env` |
|
305
|
+
| **Session Configuration** |
|
306
|
+
| `SESSION_PROVIDER` | Session metadata storage | `memory` | `redis` |
|
307
|
+
| `SESSION_REDIS_URL` | Redis connection URL | - | `redis://localhost:6379/0` |
|
308
|
+
| **AWS/S3 Configuration** |
|
309
|
+
| `AWS_ACCESS_KEY_ID` | AWS access key | - | `AKIA...` |
|
310
|
+
| `AWS_SECRET_ACCESS_KEY` | AWS secret key | - | `abc123...` |
|
311
|
+
| `AWS_REGION` | AWS region | `us-east-1` | `us-west-2`, `eu-west-1` |
|
312
|
+
| `S3_ENDPOINT_URL` | Custom S3 endpoint | - | `https://minio.example.com` |
|
313
|
+
| **IBM COS Configuration** |
|
314
|
+
| `IBM_COS_ENDPOINT` | IBM COS endpoint | - | `https://s3.us-south.cloud-object-storage.appdomain.cloud` |
|
315
|
+
| `IBM_COS_APIKEY` | IBM Cloud API key (IAM) | - | `abc123...` |
|
316
|
+
| `IBM_COS_INSTANCE_CRN` | COS instance CRN (IAM) | - | `crn:v1:bluemix:public:...` |
|
317
|
+
|
318
|
+
## Configuration Examples
|
319
|
+
|
320
|
+
### Development Setup
|
321
|
+
|
322
|
+
```python
|
323
|
+
# Zero configuration - uses memory providers
|
324
|
+
from chuk_artifacts import ArtifactStore
|
325
|
+
|
326
|
+
store = ArtifactStore()
|
327
|
+
```
|
328
|
+
|
329
|
+
### Local Development with Persistence
|
330
|
+
|
331
|
+
```python
|
332
|
+
import os
|
333
|
+
from chuk_artifacts import ArtifactStore
|
334
|
+
|
335
|
+
# Use filesystem for persistence
|
336
|
+
os.environ["ARTIFACT_PROVIDER"] = "filesystem"
|
337
|
+
os.environ["ARTIFACT_FS_ROOT"] = "./dev-storage"
|
338
|
+
|
339
|
+
store = ArtifactStore()
|
340
|
+
```
|
341
|
+
|
342
|
+
### Production with S3 + Redis
|
343
|
+
|
344
|
+
```python
|
345
|
+
import os
|
346
|
+
from chuk_artifacts import ArtifactStore
|
347
|
+
|
348
|
+
# Configure S3 storage
|
349
|
+
os.environ.update({
|
350
|
+
"ARTIFACT_PROVIDER": "s3",
|
351
|
+
"AWS_ACCESS_KEY_ID": "AKIA...",
|
352
|
+
"AWS_SECRET_ACCESS_KEY": "...",
|
353
|
+
"AWS_REGION": "us-east-1",
|
354
|
+
"ARTIFACT_BUCKET": "prod-artifacts"
|
355
|
+
})
|
356
|
+
|
357
|
+
# Configure Redis sessions
|
358
|
+
os.environ.update({
|
359
|
+
"SESSION_PROVIDER": "redis",
|
360
|
+
"SESSION_REDIS_URL": "redis://prod-redis:6379/0"
|
361
|
+
})
|
362
|
+
|
363
|
+
store = ArtifactStore()
|
364
|
+
```
|
365
|
+
|
366
|
+
### Docker Compose Example
|
367
|
+
|
368
|
+
```yaml
|
369
|
+
version: '3.8'
|
370
|
+
services:
|
371
|
+
app:
|
372
|
+
image: myapp
|
373
|
+
environment:
|
374
|
+
# Storage
|
375
|
+
ARTIFACT_PROVIDER: s3
|
376
|
+
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
377
|
+
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
378
|
+
AWS_REGION: us-east-1
|
379
|
+
ARTIFACT_BUCKET: myapp-artifacts
|
380
|
+
|
381
|
+
# Sessions
|
382
|
+
SESSION_PROVIDER: redis
|
383
|
+
SESSION_REDIS_URL: redis://redis:6379/0
|
384
|
+
depends_on:
|
385
|
+
- redis
|
386
|
+
|
387
|
+
redis:
|
388
|
+
image: redis:7-alpine
|
389
|
+
volumes:
|
390
|
+
- redis_data:/data
|
391
|
+
|
392
|
+
volumes:
|
393
|
+
redis_data:
|
394
|
+
```
|
395
|
+
|
396
|
+
## Presigned URLs
|
397
|
+
|
398
|
+
Generate secure, time-limited URLs for file access without exposing your storage credentials:
|
399
|
+
|
400
|
+
```python
|
401
|
+
# Generate download URLs
|
402
|
+
url = await store.presign(file_id) # 1 hour (default)
|
403
|
+
short_url = await store.presign_short(file_id) # 15 minutes
|
404
|
+
medium_url = await store.presign_medium(file_id) # 1 hour
|
405
|
+
long_url = await store.presign_long(file_id) # 24 hours
|
406
|
+
|
407
|
+
# Generate upload URLs
|
408
|
+
upload_url, artifact_id = await store.presign_upload(
|
409
|
+
session_id="user_123",
|
410
|
+
filename="upload.jpg",
|
411
|
+
mime_type="image/jpeg"
|
412
|
+
)
|
413
|
+
|
414
|
+
# Client uploads to upload_url, then register the file
|
415
|
+
await store.register_uploaded_artifact(
|
416
|
+
artifact_id,
|
417
|
+
mime="image/jpeg",
|
418
|
+
summary="User uploaded image",
|
419
|
+
filename="upload.jpg"
|
420
|
+
)
|
421
|
+
```
|
422
|
+
|
423
|
+
## Common Use Cases
|
424
|
+
|
425
|
+
### Web Application File Uploads
|
426
|
+
|
427
|
+
```python
|
428
|
+
from chuk_artifacts import ArtifactStore
|
429
|
+
|
430
|
+
store = ArtifactStore(
|
431
|
+
storage_provider="s3",
|
432
|
+
session_provider="redis"
|
433
|
+
)
|
434
|
+
|
435
|
+
async def handle_upload(file_data: bytes, filename: str, user_id: str):
|
436
|
+
"""Handle file upload with user isolation"""
|
437
|
+
file_id = await store.store(
|
438
|
+
data=file_data,
|
439
|
+
mime="application/octet-stream",
|
440
|
+
summary=f"Uploaded: {filename}",
|
441
|
+
filename=filename,
|
442
|
+
session_id=f"user_{user_id}" # User-specific session
|
443
|
+
)
|
444
|
+
|
445
|
+
# Return download URL
|
446
|
+
download_url = await store.presign_medium(file_id)
|
447
|
+
return {"file_id": file_id, "download_url": download_url}
|
448
|
+
|
449
|
+
async def list_user_files(user_id: str):
|
450
|
+
"""List all files for a user"""
|
451
|
+
return await store.list_by_session(f"user_{user_id}")
|
452
|
+
```
|
453
|
+
|
454
|
+
### MCP Server Integration
|
455
|
+
|
456
|
+
```python
|
457
|
+
async def mcp_upload_file(data_b64: str, filename: str, session_id: str):
|
458
|
+
"""MCP tool for file uploads"""
|
459
|
+
import base64
|
460
|
+
|
461
|
+
data = base64.b64decode(data_b64)
|
462
|
+
file_id = await store.store(
|
463
|
+
data=data,
|
464
|
+
mime="application/octet-stream",
|
465
|
+
summary=f"Uploaded via MCP: {filename}",
|
466
|
+
filename=filename,
|
467
|
+
session_id=session_id
|
468
|
+
)
|
469
|
+
|
470
|
+
return {"file_id": file_id, "message": f"Uploaded {filename}"}
|
471
|
+
|
472
|
+
async def mcp_list_files(session_id: str, directory: str = ""):
|
473
|
+
"""MCP tool for listing files"""
|
474
|
+
files = await store.get_directory_contents(session_id, directory)
|
475
|
+
return {"files": [{"name": f["filename"], "size": f["bytes"]} for f in files]}
|
476
|
+
```
|
477
|
+
|
478
|
+
### Document Management
|
479
|
+
|
480
|
+
```python
|
481
|
+
async def create_document(content: str, path: str, user_id: str):
|
482
|
+
"""Create a text document"""
|
483
|
+
doc_id = await store.write_file(
|
484
|
+
content=content,
|
485
|
+
filename=path,
|
486
|
+
mime="text/plain",
|
487
|
+
summary=f"Document: {path}",
|
488
|
+
session_id=f"user_{user_id}"
|
489
|
+
)
|
490
|
+
return doc_id
|
491
|
+
|
492
|
+
async def get_document(doc_id: str):
|
493
|
+
"""Read document content"""
|
494
|
+
return await store.read_file(doc_id, as_text=True)
|
495
|
+
|
496
|
+
async def list_documents(user_id: str, folder: str = ""):
|
497
|
+
"""List documents in a folder"""
|
498
|
+
return await store.get_directory_contents(f"user_{user_id}", folder)
|
499
|
+
```
|
500
|
+
|
501
|
+
## Batch Operations
|
502
|
+
|
503
|
+
Process multiple files efficiently:
|
504
|
+
|
505
|
+
```python
|
506
|
+
# Prepare batch data
|
507
|
+
files = [
|
508
|
+
{
|
509
|
+
"data": file1_bytes,
|
510
|
+
"mime": "image/jpeg",
|
511
|
+
"summary": "Product image 1",
|
512
|
+
"filename": "products/img1.jpg"
|
513
|
+
},
|
514
|
+
{
|
515
|
+
"data": file2_bytes,
|
516
|
+
"mime": "image/jpeg",
|
517
|
+
"summary": "Product image 2",
|
518
|
+
"filename": "products/img2.jpg"
|
519
|
+
}
|
520
|
+
]
|
521
|
+
|
522
|
+
# Store all files at once
|
523
|
+
file_ids = await store.store_batch(files, session_id="product_catalog")
|
524
|
+
```
|
525
|
+
|
526
|
+
## Error Handling
|
527
|
+
|
528
|
+
```python
|
529
|
+
from chuk_artifacts import (
|
530
|
+
ArtifactStoreError,
|
531
|
+
ArtifactNotFoundError,
|
532
|
+
ProviderError,
|
533
|
+
SessionError
|
534
|
+
)
|
535
|
+
|
536
|
+
try:
|
537
|
+
data = await store.retrieve(file_id)
|
538
|
+
except ArtifactNotFoundError:
|
539
|
+
print("File not found or expired")
|
540
|
+
except ProviderError as e:
|
541
|
+
print(f"Storage error: {e}")
|
542
|
+
except SessionError as e:
|
543
|
+
print(f"Session error: {e}")
|
544
|
+
except ArtifactStoreError as e:
|
545
|
+
# This catches security violations like cross-session operations
|
546
|
+
print(f"Operation denied: {e}")
|
547
|
+
```
|
548
|
+
|
549
|
+
## Performance
|
550
|
+
|
551
|
+
Chuk Artifacts is built for high performance:
|
552
|
+
|
553
|
+
- **3,000+ operations/second** in benchmarks
|
554
|
+
- **Async/await** throughout for non-blocking I/O
|
555
|
+
- **Connection pooling** with aioboto3
|
556
|
+
- **Redis caching** for sub-millisecond metadata lookups
|
557
|
+
- **Batch operations** to reduce overhead
|
558
|
+
|
559
|
+
### Benchmark Results
|
560
|
+
```
|
561
|
+
✅ File Creation: 3,083 files/sec
|
562
|
+
✅ File Reading: 4,693 reads/sec
|
563
|
+
✅ File Copying: 1,811 copies/sec
|
564
|
+
✅ Session Listing: ~2ms for 20+ files
|
565
|
+
```
|
566
|
+
|
567
|
+
## Testing
|
568
|
+
|
569
|
+
Run the included examples to verify everything works:
|
570
|
+
|
571
|
+
```bash
|
572
|
+
# Basic functionality test
|
573
|
+
python -c "
|
574
|
+
import asyncio
|
575
|
+
from chuk_artifacts import ArtifactStore
|
576
|
+
|
577
|
+
async def test():
|
578
|
+
async with ArtifactStore() as store:
|
579
|
+
file_id = await store.store(
|
580
|
+
data=b'Hello, world!',
|
581
|
+
mime='text/plain',
|
582
|
+
summary='Test file'
|
583
|
+
)
|
584
|
+
content = await store.retrieve(file_id)
|
585
|
+
print(f'Success! Retrieved: {content.decode()}')
|
586
|
+
|
587
|
+
asyncio.run(test())
|
588
|
+
"
|
589
|
+
```
|
590
|
+
|
591
|
+
For development, use the filesystem provider for easy debugging:
|
592
|
+
|
593
|
+
```python
|
594
|
+
import tempfile
|
595
|
+
from chuk_artifacts import ArtifactStore
|
596
|
+
|
597
|
+
async def test_with_filesystem():
|
598
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
599
|
+
store = ArtifactStore(
|
600
|
+
storage_provider="filesystem",
|
601
|
+
fs_root=tmpdir
|
602
|
+
)
|
603
|
+
|
604
|
+
file_id = await store.store(
|
605
|
+
data=b"Test content",
|
606
|
+
mime="text/plain",
|
607
|
+
summary="Test file"
|
608
|
+
)
|
609
|
+
|
610
|
+
# Files are visible in tmpdir for debugging
|
611
|
+
print(f"Files stored in: {tmpdir}")
|
612
|
+
```
|
613
|
+
|
614
|
+
## Security
|
615
|
+
|
616
|
+
### Session Isolation
|
617
|
+
|
618
|
+
Sessions provide strict security boundaries:
|
619
|
+
|
620
|
+
```python
|
621
|
+
# Each user gets their own session
|
622
|
+
alice_session = "user_alice"
|
623
|
+
bob_session = "user_bob"
|
624
|
+
|
625
|
+
# Users can only access their own files
|
626
|
+
alice_files = await store.list_by_session(alice_session)
|
627
|
+
bob_files = await store.list_by_session(bob_session)
|
628
|
+
|
629
|
+
# Cross-session operations are blocked
|
630
|
+
try:
|
631
|
+
await store.copy_file(alice_file_id, target_session_id=bob_session)
|
632
|
+
except ArtifactStoreError:
|
633
|
+
print("✅ Cross-session access denied!")
|
634
|
+
```
|
635
|
+
|
636
|
+
### Secure Defaults
|
637
|
+
|
638
|
+
- Files expire automatically (configurable TTL)
|
639
|
+
- Presigned URLs have time limits
|
640
|
+
- No sensitive data in error messages
|
641
|
+
- Environment-based credential configuration
|
642
|
+
- Session-based access control
|
643
|
+
|
644
|
+
## Migration Guide
|
645
|
+
|
646
|
+
### From Local Storage
|
647
|
+
|
648
|
+
```python
|
649
|
+
# Before: Simple file operations
|
650
|
+
with open("file.txt", "rb") as f:
|
651
|
+
data = f.read()
|
652
|
+
|
653
|
+
# After: Session-based storage
|
654
|
+
file_id = await store.store(
|
655
|
+
data=data,
|
656
|
+
mime="text/plain",
|
657
|
+
summary="Migrated file",
|
658
|
+
filename="file.txt",
|
659
|
+
session_id="migration_session"
|
660
|
+
)
|
661
|
+
```
|
662
|
+
|
663
|
+
### From Basic S3
|
664
|
+
|
665
|
+
```python
|
666
|
+
# Before: Direct S3 operations
|
667
|
+
s3.put_object(Bucket="bucket", Key="key", Body=data)
|
668
|
+
|
669
|
+
# After: Managed artifact storage
|
670
|
+
file_id = await store.store(
|
671
|
+
data=data,
|
672
|
+
mime="application/octet-stream",
|
673
|
+
summary="File description",
|
674
|
+
filename="myfile.dat"
|
675
|
+
)
|
676
|
+
```
|
677
|
+
|
678
|
+
## FAQ
|
679
|
+
|
680
|
+
### Q: Do I need Redis for development?
|
681
|
+
|
682
|
+
**A:** No! The default memory providers work great for development and testing. Only use Redis for production or when you need persistence.
|
683
|
+
|
684
|
+
### Q: Can I switch storage providers later?
|
685
|
+
|
686
|
+
**A:** Yes! Change the `ARTIFACT_PROVIDER` environment variable. The API stays the same.
|
687
|
+
|
688
|
+
### Q: How do sessions work with authentication?
|
689
|
+
|
690
|
+
**A:** Sessions are just strings. Map them to your users however you want:
|
691
|
+
|
692
|
+
```python
|
693
|
+
# Example mappings
|
694
|
+
session_id = f"user_{user.id}" # User-based
|
695
|
+
session_id = f"org_{org.id}" # Organization-based
|
696
|
+
session_id = f"project_{project.uuid}" # Project-based
|
697
|
+
```
|
698
|
+
|
699
|
+
### Q: What happens when files expire?
|
700
|
+
|
701
|
+
**A:** Expired files are automatically cleaned up during session cleanup operations. You can also run manual cleanup:
|
702
|
+
|
703
|
+
```python
|
704
|
+
expired_count = await store.cleanup_expired_sessions()
|
705
|
+
```
|
706
|
+
|
707
|
+
### Q: Can I use this with Django/FastAPI/Flask?
|
708
|
+
|
709
|
+
**A:** Absolutely! Chuk Artifacts is framework-agnostic. Initialize the store at startup and use it in your request handlers.
|
710
|
+
|
711
|
+
### Q: Is it production ready?
|
712
|
+
|
713
|
+
**A:** Yes! It's designed for production with:
|
714
|
+
- High performance (3,000+ ops/sec)
|
715
|
+
- Multiple storage backends
|
716
|
+
- Session-based security
|
717
|
+
- Comprehensive error handling
|
718
|
+
- Redis support for clustering
|
719
|
+
|
720
|
+
---
|
721
|
+
|
722
|
+
## Next Steps
|
723
|
+
|
724
|
+
1. **Try it out**: `pip install chuk-artifacts`
|
725
|
+
2. **Start simple**: Use the default memory providers
|
726
|
+
3. **Add persistence**: Switch to filesystem or S3
|
727
|
+
4. **Scale up**: Add Redis for production
|
728
|
+
5. **Secure it**: Use session-based isolation
|
729
|
+
|
730
|
+
**Ready to build something awesome?** 🚀
|