@@ -174,13 +174,90 @@ def _execute_single_notebook(
174174 except Exception as e :
175175 return {"success" : False , "error" : f"Failed to read executed notebook: { e } " , "cells" : {}}
176176
177- # Extract outputs from each cell
178- # Skip the first code cell (injected setup) when mapping indices
177+ # Extract outputs from each cell, skipping the injected setup cell
178+ cells_output = _extract_cell_outputs (executed_nb , name , figures_dir , skip_setup_cell = True )
179+
180+ # Clean up temporary executed notebook
181+ if result .get ("output_path" ):
182+ try :
183+ result ["output_path" ].unlink ()
184+ except Exception :
185+ pass
186+
187+ duration = int ((time .time () - start_time ) * 1000 )
188+
189+ output_data = {
190+ "cells" : cells_output ,
191+ "executedAt" : datetime .now (timezone .utc ).isoformat (),
192+ "duration" : duration ,
193+ "success" : True ,
194+ }
195+
196+ _save_output (outputs_dir / f"{ name } .json" , output_data )
197+
198+ return output_data
199+
200+
201+ def save_stored_outputs (notebook_path : Path , version_dir : Path ) -> dict [str , Any ] | None :
202+ """
203+ Save the outputs stored in a notebook that is not executed by this build.
204+
205+ Non-executable notebooks ship with their outputs, but the copied notebooks
206+ are stripped. The stored outputs are written in the same format as the
207+ outputs of executed notebooks, so the frontend shows them the same way.
208+
209+ Returns the output data, or None if the notebook has no stored outputs.
210+ """
211+ name = notebook_path .stem
212+
213+ try :
214+ with open (notebook_path , "r" , encoding = "utf-8" ) as f :
215+ notebook = json .load (f )
216+ except Exception as e :
217+ return {"success" : False , "error" : f"Failed to read notebook: { e } " , "cells" : {}}
218+
219+ outputs_dir = version_dir / "outputs"
220+ figures_dir = version_dir / "figures"
221+ figures_dir .mkdir (parents = True , exist_ok = True )
222+
223+ cells_output = _extract_cell_outputs (notebook , name , figures_dir , skip_setup_cell = False )
224+ if not cells_output :
225+ return None
226+
227+ output_data = {
228+ "cells" : cells_output ,
229+ "executedAt" : datetime .now (timezone .utc ).isoformat (),
230+ "duration" : 0 ,
231+ "success" : True ,
232+ "stored" : True ,
233+ }
234+
235+ outputs_dir .mkdir (parents = True , exist_ok = True )
236+ _save_output (outputs_dir / f"{ name } .json" , output_data )
237+
238+ return output_data
239+
240+
241+ def _extract_cell_outputs (
242+ notebook : dict ,
243+ name : str ,
244+ figures_dir : Path ,
245+ skip_setup_cell : bool ,
246+ ) -> dict [str , dict ]:
247+ """
248+ Extract stdout, stderr and figures of each code cell.
249+
250+ Cell indices refer to the original notebook. With skip_setup_cell the first
251+ code cell is the injected setup cell, it is skipped and the indices are
252+ shifted back by one.
253+
254+ Returns dict mapping cell index to its outputs, cells without output are omitted.
255+ """
179256 cells_output = {}
180257 figure_count = 0
181- setup_cell_skipped = False
258+ setup_cell_skipped = not skip_setup_cell
182259
183- for i , cell in enumerate (executed_nb .get ("cells" , [])):
260+ for i , cell in enumerate (notebook .get ("cells" , [])):
184261 if cell .get ("cell_type" ) != "code" :
185262 continue
186263
@@ -190,7 +267,7 @@ def _execute_single_notebook(
190267 continue
191268
192269 # Adjust index to match original notebook (subtract 1 for skipped setup cell)
193- original_index = i - 1
270+ original_index = i - 1 if skip_setup_cell else i
194271
195272 cell_output = {
196273 "stdout" : None ,
@@ -238,25 +315,7 @@ def _execute_single_notebook(
238315 if cell_output ["stdout" ] or cell_output ["stderr" ] or cell_output ["figures" ]:
239316 cells_output [str (original_index )] = cell_output
240317
241- # Clean up temporary executed notebook
242- if result .get ("output_path" ):
243- try :
244- result ["output_path" ].unlink ()
245- except Exception :
246- pass
247-
248- duration = int ((time .time () - start_time ) * 1000 )
249-
250- output_data = {
251- "cells" : cells_output ,
252- "executedAt" : datetime .now (timezone .utc ).isoformat (),
253- "duration" : duration ,
254- "success" : True ,
255- }
256-
257- _save_output (outputs_dir / f"{ name } .json" , output_data )
258-
259- return output_data
318+ return cells_output
260319
261320
262321def _execute_with_nbconvert (notebook_path : Path ) -> dict [str , Any ]:
0 commit comments