44from dataclasses import dataclass , field
55from typing import Any , cast
66
7+ from models .conversation_types import (
8+ BubbleContextDict ,
9+ BubbleMetadataDict ,
10+ ContextWindowStatusDict ,
11+ FileUriDict ,
12+ ModelInfoDict ,
13+ ThinkingDict ,
14+ TokenCountDict ,
15+ ToolFormerDataDict ,
16+ ToolResultEntry ,
17+ )
718from models .errors import SchemaError
819from models .from_dict_validation import (
920 require_dict ,
@@ -26,7 +37,7 @@ class Composer:
2637 name : str | None = None
2738 last_updated_at : Any = None
2839 model_config : dict [str , Any ] = field (default_factory = dict )
29- raw : dict [str , Any ] = field (default_factory = dict )
40+ _raw : dict [str , Any ] = field (default_factory = dict , repr = False )
3041
3142 @classmethod
3243 def from_dict (cls , raw : dict [str , Any ], * , composer_id : str ) -> "Composer" :
@@ -79,12 +90,19 @@ def from_dict(cls, raw: dict[str, Any], *, composer_id: str) -> "Composer":
7990 name = raw .get ("name" ),
8091 last_updated_at = raw .get ("lastUpdatedAt" ),
8192 model_config = model_config ,
82- raw = raw ,
93+ _raw = raw ,
8394 )
8495
96+ def cursor_storage_payload (self ) -> dict [str , Any ]:
97+ """Shallow copy of stored Cursor JSON for API passthrough.
98+
99+ Prefer typed accessors for field reads.
100+ """
101+ return dict (self ._raw )
102+
85103 @property
86104 def newly_created_files (self ) -> list [Any ]:
87- value = self .raw .get ("newlyCreatedFiles" )
105+ value = self ._raw .get ("newlyCreatedFiles" )
88106 if value is None :
89107 return []
90108 if not isinstance (value , list ):
@@ -98,7 +116,7 @@ def newly_created_files(self) -> list[Any]:
98116
99117 @property
100118 def code_block_data (self ) -> dict [str , Any ] | None :
101- value = self .raw .get ("codeBlockData" )
119+ value = self ._raw .get ("codeBlockData" )
102120 if value is None :
103121 return None
104122 if not isinstance (value , dict ):
@@ -113,7 +131,7 @@ def code_block_data(self) -> dict[str, Any] | None:
113131 @property
114132 def usage_data (self ) -> dict [str , Any ]:
115133 """Composer cost rollup; empty dict when absent (common)."""
116- value = self .raw .get ("usageData" )
134+ value = self ._raw .get ("usageData" )
117135 if value is None :
118136 return {}
119137 if not isinstance (value , dict ):
@@ -127,9 +145,9 @@ def usage_data(self) -> dict[str, Any]:
127145 return value
128146
129147 def _optional_counter (self , key : str ) -> int | float :
130- value = self .raw .get (key , 0 )
148+ value = self ._raw .get (key , 0 )
131149 if isinstance (value , bool ) or not isinstance (value , (int , float )):
132- if key in self .raw :
150+ if key in self ._raw :
133151 suffix = f" { self .composer_id } " if self .composer_id else ""
134152 _logger .warning (
135153 "Schema drift in Composer%s: invalid type for %s (expected number, got %s)" ,
@@ -172,7 +190,7 @@ class WorkspaceLocalComposer:
172190
173191 composer_id : str
174192 last_updated_at : Any = None
175- raw : dict [str , Any ] = field (default_factory = dict )
193+ _raw : dict [str , Any ] = field (default_factory = dict , repr = False )
176194
177195 @classmethod
178196 def from_dict (cls , raw : dict [str , Any ]) -> "WorkspaceLocalComposer" :
@@ -194,9 +212,13 @@ def from_dict(cls, raw: dict[str, Any]) -> "WorkspaceLocalComposer":
194212 return cls (
195213 composer_id = composer_id ,
196214 last_updated_at = raw .get ("lastUpdatedAt" ),
197- raw = raw ,
215+ _raw = raw ,
198216 )
199217
218+ def cursor_storage_payload (self ) -> dict [str , Any ]:
219+ """Shallow copy of stored Cursor JSON for API passthrough."""
220+ return dict (self ._raw )
221+
200222
201223@dataclass (frozen = True )
202224class Bubble :
@@ -206,7 +228,7 @@ class Bubble:
206228 """
207229
208230 bubble_id : str
209- raw : dict [str , Any ] = field (default_factory = dict )
231+ _raw : dict [str , Any ] = field (default_factory = dict , repr = False )
210232
211233 @classmethod
212234 def from_dict (cls , raw : dict [str , Any ], * , bubble_id : str ) -> "Bubble" :
@@ -224,17 +246,24 @@ def from_dict(cls, raw: dict[str, Any], *, bubble_id: str) -> "Bubble":
224246 """
225247 raw = require_dict (raw , model = "Bubble" , field = "bubble" )
226248 require_non_empty_str (bubble_id , model = "Bubble" , field = "bubbleId" )
227- return cls (bubble_id = bubble_id , raw = raw )
249+ return cls (bubble_id = bubble_id , _raw = raw )
250+
251+ def cursor_storage_payload (self ) -> dict [str , Any ]:
252+ """Shallow copy of stored Cursor JSON for API passthrough.
253+
254+ Prefer typed accessors for field reads.
255+ """
256+ return dict (self ._raw )
228257
229258 @property
230259 def text (self ) -> str | None :
231260 """Plain ``text`` field; richText is handled by :func:`extract_text_from_bubble`."""
232- value = self .raw .get ("text" )
261+ value = self ._raw .get ("text" )
233262 return value if isinstance (value , str ) else None
234263
235264 @property
236- def metadata (self ) -> dict [ str , Any ] :
237- value = self .raw .get ("metadata" )
265+ def metadata (self ) -> BubbleMetadataDict :
266+ value = self ._raw .get ("metadata" )
238267 if value is None :
239268 return {}
240269 if not isinstance (value , dict ):
@@ -244,11 +273,11 @@ def metadata(self) -> dict[str, Any]:
244273 type (value ).__name__ ,
245274 )
246275 return {}
247- return value
276+ return cast ( BubbleMetadataDict , value )
248277
249278 @property
250- def relevant_files (self ) -> list [Any ]:
251- value = self .raw .get ("relevantFiles" )
279+ def relevant_files (self ) -> list [str ]:
280+ value = self ._raw .get ("relevantFiles" )
252281 if value is None :
253282 return []
254283 if not isinstance (value , list ):
@@ -258,11 +287,11 @@ def relevant_files(self) -> list[Any]:
258287 type (value ).__name__ ,
259288 )
260289 return []
261- return value
290+ return cast ( list [ str ], value )
262291
263292 @property
264- def attached_file_code_chunks_uris (self ) -> list [Any ]:
265- value = self .raw .get ("attachedFileCodeChunksUris" )
293+ def attached_file_code_chunks_uris (self ) -> list [FileUriDict ]:
294+ value = self ._raw .get ("attachedFileCodeChunksUris" )
266295 if value is None :
267296 return []
268297 if not isinstance (value , list ):
@@ -272,11 +301,11 @@ def attached_file_code_chunks_uris(self) -> list[Any]:
272301 type (value ).__name__ ,
273302 )
274303 return []
275- return value
304+ return cast ( list [ FileUriDict ], value )
276305
277306 @property
278- def context (self ) -> dict [ str , Any ] :
279- value = self .raw .get ("context" )
307+ def context (self ) -> BubbleContextDict :
308+ value = self ._raw .get ("context" )
280309 if value is None :
281310 return {}
282311 if not isinstance (value , dict ):
@@ -286,11 +315,11 @@ def context(self) -> dict[str, Any]:
286315 type (value ).__name__ ,
287316 )
288317 return {}
289- return value
318+ return cast ( BubbleContextDict , value )
290319
291320 @property
292- def token_count (self ) -> dict [ str , Any ] | None :
293- value = self .raw .get ("tokenCount" )
321+ def token_count (self ) -> TokenCountDict | None :
322+ value = self ._raw .get ("tokenCount" )
294323 if value is None :
295324 return None
296325 if not isinstance (value , dict ):
@@ -300,11 +329,11 @@ def token_count(self) -> dict[str, Any] | None:
300329 type (value ).__name__ ,
301330 )
302331 return None
303- return value
332+ return cast ( TokenCountDict , value )
304333
305334 @property
306- def tool_former_data (self ) -> dict [ str , Any ] | None :
307- value = self .raw .get ("toolFormerData" )
335+ def tool_former_data (self ) -> ToolFormerDataDict | None :
336+ value = self ._raw .get ("toolFormerData" )
308337 if value is None :
309338 return None
310339 if not isinstance (value , dict ):
@@ -314,11 +343,11 @@ def tool_former_data(self) -> dict[str, Any] | None:
314343 type (value ).__name__ ,
315344 )
316345 return None
317- return value
346+ return cast ( ToolFormerDataDict , value )
318347
319348 @property
320- def model_info (self ) -> dict [ str , Any ] :
321- value = self .raw .get ("modelInfo" )
349+ def model_info (self ) -> ModelInfoDict :
350+ value = self ._raw .get ("modelInfo" )
322351 if value is None :
323352 return {}
324353 if not isinstance (value , dict ):
@@ -328,15 +357,15 @@ def model_info(self) -> dict[str, Any]:
328357 type (value ).__name__ ,
329358 )
330359 return {}
331- return value
360+ return cast ( ModelInfoDict , value )
332361
333362 @property
334- def thinking (self ) -> str | dict [ str , Any ] | None :
335- value = self .raw .get ("thinking" )
363+ def thinking (self ) -> str | ThinkingDict | None :
364+ value = self ._raw .get ("thinking" )
336365 if value is None :
337366 return None
338367 if isinstance (value , (str , dict )):
339- return value
368+ return cast ( str | ThinkingDict , value )
340369 _logger .warning (
341370 "Schema drift in Bubble %s: invalid type for thinking (expected str or dict, got %s)" ,
342371 self .bubble_id ,
@@ -346,7 +375,7 @@ def thinking(self) -> str | dict[str, Any] | None:
346375
347376 @property
348377 def thinking_duration_ms (self ) -> int | float | None :
349- value = self .raw .get ("thinkingDurationMs" )
378+ value = self ._raw .get ("thinkingDurationMs" )
350379 if value is None :
351380 return None
352381 if isinstance (value , bool ) or not isinstance (value , (int , float )):
@@ -359,8 +388,8 @@ def thinking_duration_ms(self) -> int | float | None:
359388 return cast (int | float , value )
360389
361390 @property
362- def context_window_status_at_creation (self ) -> dict [ str , Any ] :
363- value = self .raw .get ("contextWindowStatusAtCreation" )
391+ def context_window_status_at_creation (self ) -> ContextWindowStatusDict :
392+ value = self ._raw .get ("contextWindowStatusAtCreation" )
364393 if value is None :
365394 return {}
366395 if not isinstance (value , dict ):
@@ -370,11 +399,11 @@ def context_window_status_at_creation(self) -> dict[str, Any]:
370399 type (value ).__name__ ,
371400 )
372401 return {}
373- return value
402+ return cast ( ContextWindowStatusDict , value )
374403
375404 @property
376- def tool_results (self ) -> list [Any ] | None :
377- value = self .raw .get ("toolResults" )
405+ def tool_results (self ) -> list [ToolResultEntry ] | None :
406+ value = self ._raw .get ("toolResults" )
378407 if value is None :
379408 return None
380409 if not isinstance (value , list ):
@@ -384,12 +413,12 @@ def tool_results(self) -> list[Any] | None:
384413 type (value ).__name__ ,
385414 )
386415 return None
387- return value
416+ return cast ( list [ ToolResultEntry ], value )
388417
389418 def bubble_timestamp_ms (self ) -> int | float | None :
390419 """``createdAt`` or ``timestamp`` in milliseconds when present."""
391420 for key in ("createdAt" , "timestamp" ):
392- value = self .raw .get (key )
421+ value = self ._raw .get (key )
393422 if isinstance (value , (int , float )) and not isinstance (value , bool ):
394423 return value
395424 return None
0 commit comments