@@ -32,23 +32,35 @@ def optional_raw_value(
3232 model : str ,
3333 entity_id : str = "" ,
3434 expected_type : type [Any ] | tuple [type [Any ], ...] | None = None ,
35+ warn_if_missing : bool = True ,
3536) -> Any | None :
3637 """Return ``raw[key]`` when present and typed; log drift and return ``None`` otherwise."""
3738 if key not in raw :
38- warn_missing_raw_key (raw , key , model = model , entity_id = entity_id )
39+ if warn_if_missing :
40+ warn_missing_raw_key (raw , key , model = model , entity_id = entity_id )
3941 return None
4042 value = raw [key ]
41- if expected_type is not None and not isinstance (value , expected_type ):
42- suffix = f" { entity_id } " if entity_id else ""
43- _logger .warning (
44- "Schema drift in %s%s: invalid type for %s (expected %s, got %s)" ,
45- model ,
46- suffix ,
47- key ,
48- expected_type ,
49- type (value ).__name__ ,
50- )
51- return None
43+ if expected_type is not None :
44+ if isinstance (value , bool ) and expected_type in ((int , float ), int , float ):
45+ suffix = f" { entity_id } " if entity_id else ""
46+ _logger .warning (
47+ "Schema drift in %s%s: invalid type for %s (expected number, got bool)" ,
48+ model ,
49+ suffix ,
50+ key ,
51+ )
52+ return None
53+ if not isinstance (value , expected_type ):
54+ suffix = f" { entity_id } " if entity_id else ""
55+ _logger .warning (
56+ "Schema drift in %s%s: invalid type for %s (expected %s, got %s)" ,
57+ model ,
58+ suffix ,
59+ key ,
60+ expected_type ,
61+ type (value ).__name__ ,
62+ )
63+ return None
5264 return value
5365
5466
@@ -58,13 +70,15 @@ def optional_raw_list(
5870 * ,
5971 model : str ,
6072 entity_id : str = "" ,
73+ warn_if_missing : bool = True ,
6174) -> list [Any ] | None :
6275 return optional_raw_value (
6376 raw ,
6477 key ,
6578 model = model ,
6679 entity_id = entity_id ,
6780 expected_type = list ,
81+ warn_if_missing = warn_if_missing ,
6882 )
6983
7084
@@ -74,16 +88,66 @@ def optional_raw_dict(
7488 * ,
7589 model : str ,
7690 entity_id : str = "" ,
91+ warn_if_missing : bool = True ,
7792) -> dict [str , Any ] | None :
7893 return optional_raw_value (
7994 raw ,
8095 key ,
8196 model = model ,
8297 entity_id = entity_id ,
8398 expected_type = dict ,
99+ warn_if_missing = warn_if_missing ,
84100 )
85101
86102
103+ def _optional_list_absent_ok (
104+ raw : dict [str , Any ],
105+ key : str ,
106+ * ,
107+ model : str ,
108+ entity_id : str = "" ,
109+ ) -> list [Any ]:
110+ """List field often omitted by Cursor; warn only on wrong type."""
111+ value = raw .get (key )
112+ if value is None :
113+ return []
114+ if not isinstance (value , list ):
115+ suffix = f" { entity_id } " if entity_id else ""
116+ _logger .warning (
117+ "Schema drift in %s%s: invalid type for %s (expected list, got %s)" ,
118+ model ,
119+ suffix ,
120+ key ,
121+ type (value ).__name__ ,
122+ )
123+ return []
124+ return value
125+
126+
127+ def _optional_dict_absent_ok (
128+ raw : dict [str , Any ],
129+ key : str ,
130+ * ,
131+ model : str ,
132+ entity_id : str = "" ,
133+ ) -> dict [str , Any ] | None :
134+ """Dict field often omitted; warn only on wrong type; ``None`` when absent."""
135+ value = raw .get (key )
136+ if value is None :
137+ return None
138+ if not isinstance (value , dict ):
139+ suffix = f" { entity_id } " if entity_id else ""
140+ _logger .warning (
141+ "Schema drift in %s%s: invalid type for %s (expected dict, got %s)" ,
142+ model ,
143+ suffix ,
144+ key ,
145+ type (value ).__name__ ,
146+ )
147+ return None
148+ return value
149+
150+
87151def optional_raw_str (
88152 raw : dict [str , Any ],
89153 key : str ,
@@ -100,29 +164,28 @@ def optional_raw_str(
100164 )
101165
102166
103- def optional_raw_number (
167+ def _optional_str_absent_ok (
104168 raw : dict [str , Any ],
105169 key : str ,
106170 * ,
107171 model : str ,
108172 entity_id : str = "" ,
109- default : int | float = 0 ,
110- ) -> int | float :
111- """Numeric composer counters; warn on missing key, return *default* when absent."""
112- if key not in raw :
113- warn_missing_raw_key (raw , key , model = model , entity_id = entity_id )
114- return default
115- value = raw [key ]
116- if isinstance (value , bool ) or not isinstance (value , (int , float )):
117- suffix = f" { entity_id } " if entity_id else ""
118- _logger .warning (
119- "Schema drift in %s%s: invalid type for %s (expected number, got %s)" ,
120- model ,
121- suffix ,
122- key ,
123- type (value ).__name__ ,
124- )
125- return default
173+ ) -> str | None :
174+ """String field often omitted on headers; warn only on wrong type."""
175+ value = raw .get (key )
176+ if value is None :
177+ return None
178+ if not isinstance (value , str ) or not value :
179+ if key in raw :
180+ suffix = f" { entity_id } " if entity_id else ""
181+ _logger .warning (
182+ "Schema drift in %s%s: invalid type for %s (expected non-empty str, got %s)" ,
183+ model ,
184+ suffix ,
185+ key ,
186+ type (value ).__name__ ,
187+ )
188+ return None
126189 return value
127190
128191
@@ -132,13 +195,12 @@ def conversation_header_bubble_id(
132195 composer_id : str = "" ,
133196) -> str | None :
134197 """``bubbleId`` from a ``fullConversationHeadersOnly`` entry."""
135- value = optional_raw_str (
198+ return _optional_str_absent_ok (
136199 header ,
137200 "bubbleId" ,
138201 model = "ConversationHeader" ,
139202 entity_id = composer_id ,
140203 )
141- return value if value else None
142204
143205
144206def message_request_context_project_layouts (
@@ -152,6 +214,7 @@ def message_request_context_project_layouts(
152214 "projectLayouts" ,
153215 model = "MessageRequestContext" ,
154216 entity_id = composer_id ,
217+ warn_if_missing = False ,
155218 )
156219
157220
@@ -165,44 +228,41 @@ def composer_headers(
165228 return data .full_conversation_headers_only
166229 if not isinstance (data , dict ):
167230 return []
168- value = data .get ("fullConversationHeadersOnly" )
169- if value is None :
170- return []
171- if not isinstance (value , list ):
172- _logger .warning (
173- "Schema drift in Composer %s: invalid type for fullConversationHeadersOnly (expected list, got %s)" ,
174- composer_id ,
175- type (value ).__name__ ,
176- )
177- return []
178- return value
231+ return _optional_list_absent_ok (
232+ data ,
233+ "fullConversationHeadersOnly" ,
234+ model = "Composer" ,
235+ entity_id = composer_id ,
236+ )
179237
180238
181239def composer_newly_created_files (data : Any , composer_id : str ) -> list [Any ]:
182240 from models .conversation import Composer
183241
184242 if isinstance (data , Composer ):
185243 return data .newly_created_files
186- value = data .get ("newlyCreatedFiles" ) if isinstance (data , dict ) else None
187- if value is None :
188- return []
189- if not isinstance (value , list ):
190- _logger .warning (
191- "Schema drift in Composer %s: invalid type for newlyCreatedFiles (expected list, got %s)" ,
192- composer_id ,
193- type (value ).__name__ ,
194- )
244+ if not isinstance (data , dict ):
195245 return []
196- return value
246+ return _optional_list_absent_ok (
247+ data ,
248+ "newlyCreatedFiles" ,
249+ model = "Composer" ,
250+ entity_id = composer_id ,
251+ )
197252
198253
199254def composer_code_block_data (data : Any , composer_id : str ) -> dict [str , Any ] | None :
200255 from models .conversation import Composer
201256
202257 if isinstance (data , Composer ):
203258 return data .code_block_data
204- return optional_raw_dict (
205- data , "codeBlockData" , model = "Composer" , entity_id = composer_id
259+ if not isinstance (data , dict ):
260+ return None
261+ return _optional_dict_absent_ok (
262+ data ,
263+ "codeBlockData" ,
264+ model = "Composer" ,
265+ entity_id = composer_id ,
206266 )
207267
208268
@@ -212,13 +272,12 @@ def bubble_relevant_files(bubble: Any, bubble_id: str = "") -> list[Any]:
212272 if isinstance (bubble , Bubble ):
213273 return bubble .relevant_files
214274 if isinstance (bubble , dict ):
215- files = optional_raw_list (
275+ return _optional_list_absent_ok (
216276 bubble ,
217277 "relevantFiles" ,
218278 model = "Bubble" ,
219279 entity_id = bubble_id ,
220280 )
221- return files if files is not None else []
222281 return []
223282
224283
@@ -228,13 +287,12 @@ def bubble_attached_file_uris(bubble: Any, bubble_id: str = "") -> list[Any]:
228287 if isinstance (bubble , Bubble ):
229288 return bubble .attached_file_code_chunks_uris
230289 if isinstance (bubble , dict ):
231- uris = optional_raw_list (
290+ return _optional_list_absent_ok (
232291 bubble ,
233292 "attachedFileCodeChunksUris" ,
234293 model = "Bubble" ,
235294 entity_id = bubble_id ,
236295 )
237- return uris if uris is not None else []
238296 return []
239297
240298
@@ -244,7 +302,7 @@ def bubble_context(bubble: Any, bubble_id: str = "") -> dict[str, Any]:
244302 if isinstance (bubble , Bubble ):
245303 return bubble .context
246304 if isinstance (bubble , dict ):
247- ctx = optional_raw_dict (
305+ ctx = _optional_dict_absent_ok (
248306 bubble ,
249307 "context" ,
250308 model = "Bubble" ,
0 commit comments