@@ -533,6 +533,52 @@ def execute_python(
533533 duration_ms = duration_ms ,
534534 files = workspace_snapshot ,
535535 )
536+
537+ def execute_python_streaming (
538+ self ,
539+ * ,
540+ code : str ,
541+ stdin : str | None ,
542+ timeout_ms : int ,
543+ max_output_bytes : int ,
544+ cpu_time_limit_sec : int | None = None ,
545+ memory_limit_mb : int | None = None ,
546+ files : Sequence [tuple [str , bytes ]] | None = None ,
547+ last_line_interactive : bool = True ,
548+ ) -> Generator [StreamEvent , None , None ]:
549+ """Execute Python code and yield output chunks as they arrive.
550+
551+ Yields StreamChunk events during execution, then a single StreamResult
552+ at the end containing exit_code, timing, and workspace files.
553+ """
554+ with self ._run_in_pod (
555+ code = code ,
556+ cpu_time_limit_sec = cpu_time_limit_sec ,
557+ memory_limit_mb = memory_limit_mb ,
558+ files = files ,
559+ last_line_interactive = last_line_interactive ,
560+ ) as ctx :
561+ if stdin :
562+ logger .debug ("Writing stdin to Python process" )
563+ ctx .exec_resp .write_stdin (stdin )
564+
565+ deadline = time .time () + (timeout_ms / 1000.0 )
566+ exit_code , timed_out = yield from _stream_kube_output (
567+ ctx .exec_resp , deadline , max_output_bytes
568+ )
569+
570+ if timed_out :
571+ self ._kill_python_process (ctx .pod_name )
572+
573+ workspace_snapshot = self ._extract_workspace_snapshot (ctx .pod_name )
574+
575+ duration_ms = int ((time .perf_counter () - ctx .start ) * 1000 )
576+ yield StreamResult (
577+ exit_code = exit_code if not timed_out else None ,
578+ timed_out = timed_out ,
579+ duration_ms = duration_ms ,
580+ files = workspace_snapshot ,
581+ )
536582
537583 def _validate_relative_path (self , path_str : str ) -> Path :
538584 path = Path (path_str )
0 commit comments