chuk-artifacts 0.1.2__py3-none-any.whl → 0.1.3__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.
@@ -0,0 +1,196 @@
1
+ # -*- coding: utf-8 -*-
2
+ # chuk_artifacts/session/session_manager.py
3
+ """
4
+ Clean session manager for grid architecture.
5
+
6
+ Simple rules:
7
+ - Always have a session (auto-allocate if needed)
8
+ - Grid paths: grid/{sandbox_id}/{session_id}/{artifact_id}
9
+ - No legacy compatibility, clean implementation
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import uuid
15
+ import time
16
+ import json
17
+ import asyncio
18
+ import logging
19
+ from datetime import datetime, timedelta
20
+ from typing import Optional, Dict, Any, List, AsyncContextManager, Callable
21
+ from dataclasses import dataclass, asdict
22
+
23
+ from ..exceptions import SessionError, ArtifactStoreError
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _DEFAULT_SESSION_TTL_HOURS = 24
28
+
29
+
30
+ @dataclass
31
+ class SessionMetadata:
32
+ """Session metadata for grid operations."""
33
+ session_id: str
34
+ sandbox_id: str
35
+ user_id: Optional[str] = None
36
+ created_at: str = None
37
+ expires_at: str = None
38
+ status: str = "active"
39
+ artifact_count: int = 0
40
+ total_bytes: int = 0
41
+
42
+ def __post_init__(self):
43
+ if self.created_at is None:
44
+ self.created_at = datetime.utcnow().isoformat() + "Z"
45
+
46
+ def is_expired(self) -> bool:
47
+ """Check if session has expired."""
48
+ if not self.expires_at:
49
+ return False
50
+ expires = datetime.fromisoformat(self.expires_at.replace("Z", ""))
51
+ return datetime.utcnow() > expires
52
+
53
+ def to_dict(self) -> Dict[str, Any]:
54
+ return asdict(self)
55
+
56
+ @classmethod
57
+ def from_dict(cls, data: Dict[str, Any]) -> 'SessionMetadata':
58
+ return cls(**data)
59
+
60
+
61
+ class SessionManager:
62
+ """Simple session manager for grid architecture."""
63
+
64
+ def __init__(
65
+ self,
66
+ sandbox_id: str,
67
+ session_factory: Callable[[], AsyncContextManager],
68
+ default_ttl_hours: int = _DEFAULT_SESSION_TTL_HOURS,
69
+ ):
70
+ self.sandbox_id = sandbox_id
71
+ self.session_factory = session_factory
72
+ self.default_ttl_hours = default_ttl_hours
73
+
74
+ # Simple in-memory cache
75
+ self._session_cache: Dict[str, SessionMetadata] = {}
76
+ self._cache_lock = asyncio.Lock()
77
+
78
+ logger.info(f"SessionManager initialized for sandbox: {sandbox_id}")
79
+
80
+ async def allocate_session(
81
+ self,
82
+ session_id: Optional[str] = None,
83
+ user_id: Optional[str] = None,
84
+ ttl_hours: Optional[int] = None,
85
+ ) -> str:
86
+ """Allocate or validate a session."""
87
+ ttl_hours = ttl_hours or self.default_ttl_hours
88
+
89
+ if session_id:
90
+ # Validate existing session
91
+ metadata = await self._get_session_metadata(session_id)
92
+ if metadata and not metadata.is_expired():
93
+ await self._touch_session(session_id)
94
+ return session_id
95
+
96
+ # Create new session
97
+ if not session_id:
98
+ session_id = self._generate_session_id(user_id)
99
+
100
+ expires_at = (datetime.utcnow() + timedelta(hours=ttl_hours)).isoformat() + "Z"
101
+
102
+ metadata = SessionMetadata(
103
+ session_id=session_id,
104
+ sandbox_id=self.sandbox_id,
105
+ user_id=user_id,
106
+ expires_at=expires_at,
107
+ )
108
+
109
+ await self._store_session_metadata(metadata)
110
+
111
+ async with self._cache_lock:
112
+ self._session_cache[session_id] = metadata
113
+
114
+ logger.info(f"Session allocated: {session_id} (user: {user_id})")
115
+ return session_id
116
+
117
+ async def validate_session(self, session_id: str) -> bool:
118
+ """Check if session is valid."""
119
+ try:
120
+ metadata = await self._get_session_metadata(session_id)
121
+ if metadata and not metadata.is_expired():
122
+ await self._touch_session(session_id)
123
+ return True
124
+ return False
125
+ except Exception:
126
+ return False
127
+
128
+ async def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
129
+ """Get session information."""
130
+ metadata = await self._get_session_metadata(session_id)
131
+ return metadata.to_dict() if metadata else None
132
+
133
+ def get_canonical_prefix(self, session_id: str) -> str:
134
+ """Get grid path prefix."""
135
+ return f"grid/{self.sandbox_id}/{session_id}/"
136
+
137
+ def generate_artifact_key(self, session_id: str, artifact_id: str) -> str:
138
+ """Generate grid artifact key."""
139
+ return f"grid/{self.sandbox_id}/{session_id}/{artifact_id}"
140
+
141
+ def _generate_session_id(self, user_id: Optional[str] = None) -> str:
142
+ """Generate session ID."""
143
+ timestamp = int(time.time())
144
+ unique = uuid.uuid4().hex[:8]
145
+
146
+ if user_id:
147
+ safe_user = "".join(c for c in user_id if c.isalnum())[:8]
148
+ return f"sess-{safe_user}-{timestamp}-{unique}"
149
+ else:
150
+ return f"sess-{timestamp}-{unique}"
151
+
152
+ async def _get_session_metadata(self, session_id: str) -> Optional[SessionMetadata]:
153
+ """Get session metadata."""
154
+ # Check cache
155
+ async with self._cache_lock:
156
+ if session_id in self._session_cache:
157
+ return self._session_cache[session_id]
158
+
159
+ # Query session provider
160
+ try:
161
+ session_ctx_mgr = self.session_factory()
162
+ async with session_ctx_mgr as session:
163
+ raw_data = await session.get(f"session:{session_id}")
164
+ if raw_data:
165
+ data = json.loads(raw_data)
166
+ metadata = SessionMetadata.from_dict(data)
167
+
168
+ # Cache it
169
+ async with self._cache_lock:
170
+ self._session_cache[session_id] = metadata
171
+
172
+ return metadata
173
+ except Exception as e:
174
+ logger.warning(f"Failed to get session {session_id}: {e}")
175
+
176
+ return None
177
+
178
+ async def _store_session_metadata(self, metadata: SessionMetadata) -> None:
179
+ """Store session metadata."""
180
+ try:
181
+ session_ctx_mgr = self.session_factory()
182
+ async with session_ctx_mgr as session:
183
+ key = f"session:{metadata.session_id}"
184
+ ttl_seconds = int((datetime.fromisoformat(metadata.expires_at.replace("Z", "")) - datetime.utcnow()).total_seconds())
185
+ data = json.dumps(metadata.to_dict())
186
+
187
+ await session.setex(key, ttl_seconds, data)
188
+ except Exception as e:
189
+ raise SessionError(f"Session storage failed: {e}") from e
190
+
191
+ async def _touch_session(self, session_id: str) -> None:
192
+ """Update last accessed time."""
193
+ metadata = await self._get_session_metadata(session_id)
194
+ if metadata:
195
+ # Simple touch - could update last_accessed if we add that field
196
+ await self._store_session_metadata(metadata)
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
- # chuk_artifacts/session_operations.py (LOGGING FIX)
2
+ # chuk_artifacts/session/session_operations.py
3
3
  """
4
4
  Session-based file operations with strict session isolation.
5
- FIXED: Resolved logging conflict with 'filename' parameter.
6
5
  """
7
6
 
8
7
  from __future__ import annotations
@@ -11,8 +10,8 @@ import uuid, hashlib, json, logging
11
10
  from datetime import datetime
12
11
  from typing import Any, Dict, Optional, Union, List
13
12
 
14
- from .base import BaseOperations
15
- from .exceptions import (
13
+ from ..base import BaseOperations
14
+ from ..exceptions import (
16
15
  ArtifactStoreError, ArtifactNotFoundError, ArtifactExpiredError,
17
16
  ProviderError, SessionError
18
17
  )
@@ -65,7 +64,7 @@ class SessionOperations(BaseOperations):
65
64
 
66
65
  if updates:
67
66
  # Use the metadata operations to update
68
- from .metadata import MetadataOperations
67
+ from ..metadata import MetadataOperations
69
68
  metadata_ops = MetadataOperations(self._artifact_store)
70
69
  return await metadata_ops.update_metadata(artifact_id, **updates)
71
70
 
@@ -138,7 +137,7 @@ class SessionOperations(BaseOperations):
138
137
  copy_meta["copy_within_session"] = original_session
139
138
 
140
139
  # Store the copy using core operations
141
- from .core import CoreStorageOperations
140
+ from ..core import CoreStorageOperations
142
141
  core_ops = CoreStorageOperations(self._artifact_store)
143
142
 
144
143
  new_artifact_id = await core_ops.store(
@@ -258,7 +257,7 @@ class SessionOperations(BaseOperations):
258
257
  session_id = session_id or existing_session
259
258
 
260
259
  # Delete old version (within same session)
261
- from .metadata import MetadataOperations
260
+ from ..metadata import MetadataOperations
262
261
  metadata_ops = MetadataOperations(self._artifact_store)
263
262
  await metadata_ops.delete(overwrite_artifact_id)
264
263
 
@@ -266,7 +265,7 @@ class SessionOperations(BaseOperations):
266
265
  pass # Original doesn't exist, proceed with new creation
267
266
 
268
267
  # Store new content using core operations
269
- from .core import CoreStorageOperations
268
+ from ..core import CoreStorageOperations
270
269
  core_ops = CoreStorageOperations(self._artifact_store)
271
270
 
272
271
  write_meta = {**(meta or {})}
@@ -327,7 +326,7 @@ class SessionOperations(BaseOperations):
327
326
  List files in a directory-like structure within a session.
328
327
  """
329
328
  try:
330
- from .metadata import MetadataOperations
329
+ from ..metadata import MetadataOperations
331
330
  metadata_ops = MetadataOperations(self._artifact_store)
332
331
  return await metadata_ops.list_by_prefix(session_id, directory_prefix, limit)
333
332
  except Exception as e:
@@ -345,7 +344,7 @@ class SessionOperations(BaseOperations):
345
344
 
346
345
  async def _retrieve_data(self, artifact_id: str) -> bytes:
347
346
  """Helper to retrieve artifact data using core operations."""
348
- from .core import CoreStorageOperations
347
+ from ..core import CoreStorageOperations
349
348
  core_ops = CoreStorageOperations(self._artifact_store)
350
349
  return await core_ops.retrieve(artifact_id)
351
350