1use std::{
14 io::{Read, Write},
15 path::{Path, PathBuf},
16 process::{Command as StdCommand, Stdio as StdStdio},
17 sync::Arc,
18 thread,
19 time::{Duration, Instant},
20};
21
22use async_trait::async_trait;
23use serde_json::Value;
24use soma_provider_core::{
25 Provider, ProviderCall, ProviderCatalog, ProviderError, ProviderOutput, ProviderTool,
26 validate_provider_manifest_value,
27};
28use tokio::time::Instant as TokioInstant;
29
30use crate::{
31 error::{SidecarError, redact_public},
32 python_bridge::python_bridge_program,
33 python_protocol::{
34 PYTHON_PROTOCOL_HEADROOM_BYTES, PythonProtocolError, PythonWorkerRequest,
35 PythonWorkerResponse, decode_python_response, encode_python_request,
36 validate_python_response,
37 },
38 sidecar::{
39 collect_provider_env, output_exceeded_message, resolve_sidecar_command,
40 run_bounded_sidecar, sidecar_base_env, spawn_retrying_busy_image_blocking,
41 },
42};
43use supervisor::{PythonSupervisorConfig, PythonWorkerIdentity, PythonWorkerSupervisor};
44
45pub mod cache;
46mod catalog;
47mod containment;
48pub mod environment;
49pub mod host;
50mod interpreter;
51pub mod lifecycle;
52pub mod materializer;
53pub mod supervisor;
54pub use interpreter::PythonInterpreter;
55pub(crate) use interpreter::default_python_command;
56use interpreter::select_python_command;
57
58const DEFAULT_TIMEOUT_MS: u64 = 10_000;
59const DEFAULT_MAX_INPUT_BYTES: usize = 64 * 1024;
60const DEFAULT_MAX_OUTPUT_BYTES: usize = 256 * 1024;
61
62pub use catalog::describe_persistent_catalog;
63
64fn protocol_output_limit(payload_limit: usize) -> usize {
65 payload_limit.saturating_add(PYTHON_PROTOCOL_HEADROOM_BYTES)
66}
67
68#[derive(Clone)]
69pub struct PythonProvider {
70 path: PathBuf,
71 catalog: ProviderCatalog,
72 env_prefix: String,
73 interpreter: PythonInterpreter,
74 supervisor: Option<Arc<PythonWorkerSupervisor>>,
75}
76
77impl PythonProvider {
78 pub async fn preflight(&self) -> Result<(), ProviderError> {
81 let Some(supervisor) = &self.supervisor else {
82 return Ok(());
83 };
84 supervisor.preflight().await.map(|_| ()).map_err(|error| {
85 ProviderError::new(
86 error.code(),
87 &self.catalog.provider.name,
88 None,
89 error.to_string(),
90 "Install the matching soma-provider wheel and inspect the provider source.",
91 )
92 .with_provider_kind(self.catalog.provider.kind.as_str())
93 .with_source(self.path.display().to_string())
94 .with_phase("persistent-preflight")
95 })
96 }
97
98 pub fn new(path: PathBuf, catalog: ProviderCatalog, env_prefix: impl Into<String>) -> Self {
99 Self::new_with_interpreter(path, catalog, env_prefix, PythonInterpreter::Ambient)
100 }
101
102 pub fn new_with_interpreter(
103 path: PathBuf,
104 catalog: ProviderCatalog,
105 env_prefix: impl Into<String>,
106 interpreter: PythonInterpreter,
107 ) -> Self {
108 Self {
109 path,
110 catalog,
111 env_prefix: env_prefix.into(),
112 interpreter,
113 supervisor: None,
114 }
115 }
116
117 pub fn new_persistent(
119 path: PathBuf,
120 catalog: ProviderCatalog,
121 env_prefix: impl Into<String>,
122 interpreter: PythonInterpreter,
123 config: PythonSupervisorConfig,
124 ) -> Result<Self, ProviderError> {
125 Self::new_persistent_inner(path, catalog, env_prefix, interpreter, config, None)
126 }
127
128 fn new_persistent_inner(
129 path: PathBuf,
130 catalog: ProviderCatalog,
131 env_prefix: impl Into<String>,
132 interpreter: PythonInterpreter,
133 config: PythonSupervisorConfig,
134 immutable_generation_digest: Option<String>,
135 ) -> Result<Self, ProviderError> {
136 if !catalog.env.is_empty() || catalog.tools.iter().any(|tool| !tool.env.is_empty()) {
137 return Err(ProviderError::validation(
138 &catalog.provider.name,
139 "",
140 "python_persistent_env_unsupported",
141 "Persistent Python providers cannot declare runtime environment requirements",
142 ));
143 }
144 let source = std::fs::read(&path).map_err(|error| {
145 ProviderError::execution(&catalog.provider.name, "", error)
146 .with_phase("persistent-preflight")
147 })?;
148 let source_digest = sha256_hex(&source);
149 let worker_group = immutable_generation_digest.unwrap_or_else(|| source_digest.clone());
150 if worker_group.len() != 64 || !worker_group.as_bytes().iter().all(u8::is_ascii_hexdigit) {
151 return Err(ProviderError::validation(
152 &catalog.provider.name,
153 "",
154 "python_generation_digest_invalid",
155 "immutable Python generation digest must contain exactly 64 ASCII hexadecimal characters",
156 ));
157 }
158 let catalog_fingerprint = python_catalog_fingerprint(&catalog)
159 .map_err(|error| ProviderError::execution(&catalog.provider.name, "", error))?;
160 let generation_id = format!("{}-{}", catalog.provider.name, &worker_group[..16]);
161 let supervisor = PythonWorkerSupervisor::new_with_capabilities(
162 PythonWorkerIdentity {
163 path: path.clone(),
164 generation_id,
165 worker_group,
166 source_digest,
167 catalog_fingerprint,
168 },
169 interpreter.clone(),
170 config,
171 &catalog.capabilities,
172 );
173 Ok(Self {
174 path,
175 catalog,
176 env_prefix: env_prefix.into(),
177 interpreter,
178 supervisor: Some(supervisor),
179 })
180 }
181
182 pub fn arc_persistent(
183 path: PathBuf,
184 catalog: ProviderCatalog,
185 env_prefix: impl Into<String>,
186 interpreter: PythonInterpreter,
187 config: PythonSupervisorConfig,
188 ) -> Result<Arc<Self>, ProviderError> {
189 Self::new_persistent(path, catalog, env_prefix, interpreter, config).map(Arc::new)
190 }
191
192 pub fn arc_persistent_in_generation(
195 path: PathBuf,
196 catalog: ProviderCatalog,
197 env_prefix: impl Into<String>,
198 interpreter: PythonInterpreter,
199 config: PythonSupervisorConfig,
200 immutable_generation_digest: String,
201 ) -> Result<Arc<Self>, ProviderError> {
202 Self::new_persistent_inner(
203 path,
204 catalog,
205 env_prefix,
206 interpreter,
207 config,
208 Some(immutable_generation_digest),
209 )
210 .map(Arc::new)
211 }
212
213 pub fn arc(
214 path: PathBuf,
215 catalog: ProviderCatalog,
216 env_prefix: impl Into<String>,
217 ) -> Arc<Self> {
218 Arc::new(Self::new(path, catalog, env_prefix))
219 }
220
221 pub fn arc_with_interpreter(
222 path: PathBuf,
223 catalog: ProviderCatalog,
224 env_prefix: impl Into<String>,
225 interpreter: PythonInterpreter,
226 ) -> Arc<Self> {
227 Arc::new(Self::new_with_interpreter(
228 path,
229 catalog,
230 env_prefix,
231 interpreter,
232 ))
233 }
234}
235
236fn python_catalog_fingerprint(catalog: &ProviderCatalog) -> Result<String, serde_json::Error> {
237 let mut catalog = catalog.clone();
238 catalog.provider.source = None;
241 serde_json::to_vec(&catalog).map(|bytes| sha256_hex(&bytes))
242}
243
244fn sha256_hex(bytes: &[u8]) -> String {
245 use sha2::{Digest, Sha256};
246 Sha256::digest(bytes)
247 .iter()
248 .map(|byte| format!("{byte:02x}"))
249 .collect()
250}
251
252#[async_trait]
253impl Provider for PythonProvider {
254 fn catalog(&self) -> ProviderCatalog {
255 self.catalog.clone()
256 }
257
258 async fn call(&self, call: ProviderCall) -> Result<ProviderOutput, ProviderError> {
259 let tool = self.tool(&call)?;
260 let runtime = PythonRuntime::from_tool(
261 &self.catalog,
262 tool,
263 &call,
264 &self.env_prefix,
265 &self.interpreter,
266 )?;
267 let source = self.path.display().to_string();
268 let env_keys = runtime.env.iter().map(|(key, _)| key.clone()).collect();
269 let request = PythonWorkerRequest::call(&self.path, &call, env_keys);
270 let input = encode_python_request(&request).map_err(|error| {
271 ProviderError::execution(&self.catalog.provider.name, "", error)
272 .with_provider_kind(self.catalog.provider.kind.as_str())
273 .with_source(source.clone())
274 .with_phase("input-serialization")
275 })?;
276
277 if input.len() > runtime.max_input_bytes {
278 return Err(ProviderError::validation(
279 &self.catalog.provider.name,
280 &call.action,
281 "python_input_too_large",
282 format!(
283 "Python provider input exceeds {} bytes",
284 runtime.max_input_bytes
285 ),
286 )
287 .with_provider_kind(self.catalog.provider.kind.as_str())
288 .with_source(source)
289 .with_phase("input-validation"));
290 }
291
292 if let Some(supervisor) = &self.supervisor {
293 let value = supervisor
294 .invoke_with_context(
295 &call.provider,
296 &call.action,
297 call.params.clone(),
298 supervisor::PythonInvocationOptions {
299 surface: call.surface,
300 snapshot_id: &call.snapshot_id,
301 timeout: Duration::from_millis(runtime.timeout_ms),
302 context: &call.context,
303 },
304 )
305 .await
306 .map_err(|error| {
307 ProviderError::new(
308 error.code(),
309 &self.catalog.provider.name,
310 Some(call.action.clone()),
311 error.to_string(),
312 "Inspect the Python provider and persistent worker status, then retry.",
313 )
314 .with_provider_kind(self.catalog.provider.kind.as_str())
315 .with_source(source.clone())
316 .with_phase("persistent-execution")
317 })?;
318 let payload_size = serde_json::to_vec(&value)
319 .map_err(|error| {
320 ProviderError::execution(&self.catalog.provider.name, &call.action, error)
321 })?
322 .len();
323 if payload_size > runtime.max_output_bytes {
324 return Err(ProviderError::validation(
325 &self.catalog.provider.name,
326 &call.action,
327 "python_output_too_large",
328 output_exceeded_message("result", runtime.max_output_bytes),
329 ));
330 }
331 return Ok(ProviderOutput::json(value));
332 }
333
334 let started = TokioInstant::now();
335 let sidecar = match run_bounded_sidecar(
336 &runtime.command,
337 &["-c", python_bridge_program()],
338 runtime.env,
339 &input,
340 runtime.timeout_ms,
341 protocol_output_limit(runtime.max_output_bytes),
342 )
343 .await
344 {
345 Ok(sidecar) => sidecar,
346 Err(SidecarError::Timeout) => {
347 return Err(ProviderError::new(
348 "python_provider_timeout",
349 &self.catalog.provider.name,
350 Some(call.action.clone()),
351 format!("Python provider exceeded {}ms timeout", runtime.timeout_ms),
352 "Increase tool.limits.timeout_ms or fix the Python provider handler.",
353 )
354 .with_provider_kind(self.catalog.provider.kind.as_str())
355 .with_source(source)
356 .with_phase("execution"));
357 }
358 Err(error) => {
359 return Err(ProviderError::execution(
360 &self.catalog.provider.name,
361 call.action.clone(),
362 error,
363 )
364 .with_provider_kind(self.catalog.provider.kind.as_str())
365 .with_source(source)
366 .with_phase("execution"));
367 }
368 };
369 let output = sidecar.output;
370
371 tracing::debug!(
372 provider = %self.catalog.provider.name,
373 action = %call.action,
374 elapsed_ms = started.elapsed().as_millis(),
375 "Python provider sidecar completed"
376 );
377
378 if sidecar.stdout_exceeded
379 || sidecar.stderr_exceeded
380 || output.stderr.len() > runtime.max_output_bytes
381 {
382 let stream = if sidecar.stdout_exceeded {
383 "stdout"
384 } else {
385 "stderr"
386 };
387 return Err(ProviderError::validation(
388 &self.catalog.provider.name,
389 &call.action,
390 "python_output_too_large",
391 output_exceeded_message(stream, runtime.max_output_bytes),
392 )
393 .with_provider_kind(self.catalog.provider.kind.as_str())
394 .with_source(source)
395 .with_phase("output-validation"));
396 }
397 if !output.status.success() {
398 let stderr = String::from_utf8_lossy(&output.stderr);
399 let code = if stderr.contains("python_provider_unserializable_output") {
400 "python_provider_unserializable_output"
401 } else {
402 "python_provider_failed"
403 };
404 return Err(ProviderError::new(
405 code,
406 &self.catalog.provider.name,
407 Some(call.action),
408 format!("Python provider failed: {}", redact_public(&stderr)),
409 "Fix the Python provider handler and retry.",
410 )
411 .with_provider_kind(self.catalog.provider.kind.as_str())
412 .with_source(source)
413 .with_phase("execution"));
414 }
415
416 let response = decode_python_response(&output.stdout).map_err(|error| {
417 let code = match &error {
418 PythonProtocolError::Json(_) => "python_invalid_json_output",
419 _ => "python_protocol_mismatch",
420 };
421 ProviderError::validation(
422 &self.catalog.provider.name,
423 &call.action,
424 code,
425 error.to_string(),
426 )
427 .with_provider_kind(self.catalog.provider.kind.as_str())
428 .with_source(source.clone())
429 .with_phase("output-validation")
430 })?;
431 validate_python_response(&request, &response).map_err(|error| {
432 ProviderError::validation(
433 &self.catalog.provider.name,
434 &call.action,
435 "python_protocol_mismatch",
436 error.to_string(),
437 )
438 .with_provider_kind(self.catalog.provider.kind.as_str())
439 .with_source(source.clone())
440 .with_phase("output-validation")
441 })?;
442 let value = match response {
443 PythonWorkerResponse::Call { output, .. } => output,
444 PythonWorkerResponse::Catalog { .. } => {
445 return Err(ProviderError::validation(
446 &self.catalog.provider.name,
447 &call.action,
448 "python_protocol_mismatch",
449 "Python worker returned a catalog response for a call request",
450 )
451 .with_provider_kind(self.catalog.provider.kind.as_str())
452 .with_source(source)
453 .with_phase("output-validation"));
454 }
455 };
456 let payload_size = serde_json::to_vec(&value)
457 .map_err(|error| {
458 ProviderError::execution(&self.catalog.provider.name, &call.action, error)
459 .with_provider_kind(self.catalog.provider.kind.as_str())
460 .with_source(source.clone())
461 .with_phase("output-serialization")
462 })?
463 .len();
464 if payload_size > runtime.max_output_bytes {
465 return Err(ProviderError::validation(
466 &self.catalog.provider.name,
467 &call.action,
468 "python_output_too_large",
469 output_exceeded_message("stdout", runtime.max_output_bytes),
470 )
471 .with_provider_kind(self.catalog.provider.kind.as_str())
472 .with_source(source)
473 .with_phase("output-validation"));
474 }
475 Ok(ProviderOutput::json(value))
476 }
477
478 fn runtime_status(&self) -> Option<Value> {
479 self.supervisor
480 .as_ref()
481 .and_then(|supervisor| serde_json::to_value(supervisor.status()).ok())
482 }
483
484 fn cancel_active(&self) -> bool {
485 self.supervisor
486 .as_ref()
487 .is_some_and(|supervisor| supervisor.cancel_active())
488 }
489
490 async fn reset_quarantine(&self) {
491 if let Some(supervisor) = &self.supervisor {
492 supervisor.reset_quarantine().await;
493 }
494 }
495
496 async fn suspend(&self) {
497 if let Some(supervisor) = &self.supervisor {
498 supervisor.suspend().await;
499 }
500 }
501
502 fn deactivate(&self) {
503 if let Some(supervisor) = &self.supervisor {
504 supervisor.deactivate();
505 }
506 }
507
508 fn activate(&self) {
509 if let Some(supervisor) = &self.supervisor {
510 supervisor.activate();
511 }
512 }
513
514 fn acquire_dispatch(&self) -> bool {
515 self.supervisor
516 .as_ref()
517 .is_none_or(|supervisor| supervisor.acquire_dispatch())
518 }
519
520 fn release_dispatch(&self) {
521 if let Some(supervisor) = &self.supervisor {
522 supervisor.release_dispatch();
523 }
524 }
525
526 async fn retire(&self) {
527 if let Some(supervisor) = &self.supervisor {
528 supervisor.drain_and_shutdown().await;
529 }
530 }
531}
532
533impl PythonProvider {
534 fn tool(&self, call: &ProviderCall) -> Result<&ProviderTool, ProviderError> {
535 self.catalog
536 .tools
537 .iter()
538 .find(|tool| tool.name == call.action)
539 .ok_or_else(|| {
540 ProviderError::validation(
541 &self.catalog.provider.name,
542 &call.action,
543 "unknown_python_action",
544 format!("Python provider has no action `{}`", call.action),
545 )
546 })
547 }
548}
549
550pub fn load_python_catalog(path: &Path, env_prefix: &str) -> Result<ProviderCatalog, String> {
557 load_python_catalog_with_interpreter(path, env_prefix, &PythonInterpreter::Ambient)
558}
559
560pub fn load_python_catalog_with_interpreter(
561 path: &Path,
562 env_prefix: &str,
563 interpreter: &PythonInterpreter,
564) -> Result<ProviderCatalog, String> {
565 let runtime = PythonRuntime::for_catalog(env_prefix, interpreter);
566 let request = PythonWorkerRequest::catalog(path);
567 let input = encode_python_request(&request).map_err(|error| error.to_string())?;
568 let output = run_catalog_sidecar(&runtime, &input)?;
569 let response = decode_python_response(&output).map_err(|error| error.to_string())?;
570 validate_python_response(&request, &response).map_err(|error| error.to_string())?;
571 let value = match response {
572 PythonWorkerResponse::Catalog { catalog, .. } => catalog,
573 PythonWorkerResponse::Call { .. } => {
574 return Err("Python worker returned a call response for a catalog request".to_owned());
575 }
576 };
577 let payload_size = serde_json::to_vec(&value)
578 .map_err(|error| error.to_string())?
579 .len();
580 if payload_size > runtime.max_output_bytes {
581 return Err(format!(
582 "Python provider catalog {}",
583 output_exceeded_message("stdout", runtime.max_output_bytes)
584 ));
585 }
586 validate_provider_manifest_value(&value).map_err(|error| error.to_string())
587}
588
589struct PythonRuntime {
590 command: String,
591 env: Vec<(String, String)>,
592 timeout_ms: u64,
593 max_input_bytes: usize,
594 max_output_bytes: usize,
595}
596
597impl PythonRuntime {
598 fn for_catalog(env_prefix: &str, interpreter: &PythonInterpreter) -> Self {
599 let prefix = env_prefix.trim_matches('_').to_ascii_uppercase();
600 let timeout_var = format!("{prefix}_PYTHON_CATALOG_TIMEOUT_MS");
601 let command_var = format!("{prefix}_PYTHON_COMMAND");
602 let timeout_ms = match std::env::var(&timeout_var) {
603 Ok(value) => value.parse().unwrap_or_else(|error| {
604 tracing::warn!(
605 variable = %timeout_var,
606 value,
607 error = %error,
608 "invalid provider catalog timeout env var; falling back to the default"
609 );
610 DEFAULT_TIMEOUT_MS
611 }),
612 Err(_) => DEFAULT_TIMEOUT_MS,
613 };
614 Self {
615 command: select_python_command(None, std::env::var(command_var).ok(), interpreter),
616 env: Vec::new(),
617 timeout_ms,
618 max_input_bytes: DEFAULT_MAX_INPUT_BYTES,
619 max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
620 }
621 }
622
623 fn from_tool(
624 catalog: &ProviderCatalog,
625 tool: &ProviderTool,
626 call: &ProviderCall,
627 env_prefix: &str,
628 interpreter: &PythonInterpreter,
629 ) -> Result<Self, ProviderError> {
630 let provider_meta = catalog.meta.get("python");
631 let tool_meta = tool.meta.get("python");
632 let meta_field = |key: &str| {
633 tool_meta
634 .and_then(|value| value.get(key))
635 .or_else(|| provider_meta.and_then(|value| value.get(key)))
636 };
637 let command = select_python_command(
638 meta_field("command").and_then(Value::as_str),
639 std::env::var(format!(
640 "{}_PYTHON_COMMAND",
641 env_prefix.trim_matches('_').to_ascii_uppercase()
642 ))
643 .ok(),
644 interpreter,
645 );
646 let timeout_ms = tool
647 .limits
648 .as_ref()
649 .and_then(|limits| limits.timeout_ms)
650 .or_else(|| meta_field("timeout_ms").and_then(Value::as_u64))
651 .unwrap_or(DEFAULT_TIMEOUT_MS);
652 let max_input_bytes = tool
653 .limits
654 .as_ref()
655 .and_then(|limits| limits.max_input_bytes)
656 .unwrap_or(DEFAULT_MAX_INPUT_BYTES);
657 let max_output_bytes = tool
658 .limits
659 .as_ref()
660 .and_then(|limits| limits.max_response_bytes)
661 .unwrap_or(DEFAULT_MAX_OUTPUT_BYTES);
662 Ok(Self {
663 command,
664 env: collect_provider_env(
665 &catalog.env,
666 &tool.env,
667 env_prefix,
668 &call.provider,
669 &call.action,
670 )?,
671 timeout_ms,
672 max_input_bytes,
673 max_output_bytes,
674 })
675 }
676}
677
678fn run_catalog_sidecar(runtime: &PythonRuntime, input: &[u8]) -> Result<Vec<u8>, String> {
679 let mut command = StdCommand::new(resolve_sidecar_command(&runtime.command));
680 command
681 .args(["-c", python_bridge_program()])
682 .env_clear()
683 .stdin(StdStdio::piped())
684 .stdout(StdStdio::piped())
685 .stderr(StdStdio::piped());
686 for (key, value) in sidecar_base_env() {
687 command.env(key, value);
688 }
689 let mut child =
690 spawn_retrying_busy_image_blocking(&mut command).map_err(|error| error.to_string())?;
691 let stdout = child
692 .stdout
693 .take()
694 .ok_or_else(|| "Python provider catalog stdout pipe was not captured".to_owned())?;
695 let stderr = child
696 .stderr
697 .take()
698 .ok_or_else(|| "Python provider catalog stderr pipe was not captured".to_owned())?;
699 let capture_limit = protocol_output_limit(runtime.max_output_bytes);
700 let stdout_task = thread::spawn(move || read_bounded_sync(stdout, capture_limit));
701 let stderr_task = thread::spawn(move || read_bounded_sync(stderr, capture_limit));
702
703 if let Some(mut stdin) = child.stdin.take() {
704 stdin.write_all(input).map_err(|error| error.to_string())?;
705 }
706 let deadline = Instant::now() + Duration::from_millis(runtime.timeout_ms);
707 loop {
708 if let Some(status) = child.try_wait().map_err(|error| error.to_string())? {
709 let (stdout, stdout_exceeded) = stdout_task
710 .join()
711 .map_err(|_| "Python provider catalog stdout reader panicked".to_owned())?
712 .map_err(|error| error.to_string())?;
713 let (stderr, stderr_exceeded) = stderr_task
714 .join()
715 .map_err(|_| "Python provider catalog stderr reader panicked".to_owned())?
716 .map_err(|error| error.to_string())?;
717 if stdout_exceeded || stderr_exceeded || stderr.len() > runtime.max_output_bytes {
718 let stream = if stdout_exceeded { "stdout" } else { "stderr" };
719 return Err(format!(
720 "Python provider catalog {}",
721 output_exceeded_message(stream, runtime.max_output_bytes)
722 ));
723 }
724 if !status.success() {
725 return Err(format!(
726 "Python provider catalog failed: {}",
727 redact_public(&String::from_utf8_lossy(&stderr))
728 ));
729 }
730 return Ok(stdout);
731 }
732 if Instant::now() >= deadline {
733 let _ = child.kill();
734 let _ = child.wait();
735 return Err(format!(
736 "Python provider catalog exceeded {}ms timeout",
737 runtime.timeout_ms
738 ));
739 }
740 std::thread::sleep(Duration::from_millis(10));
741 }
742}
743
744fn read_bounded_sync<R: Read>(
745 mut reader: R,
746 max_output_bytes: usize,
747) -> std::io::Result<(Vec<u8>, bool)> {
748 let mut bytes = Vec::new();
749 let mut exceeded = false;
750 let mut chunk = [0u8; 8192];
751 loop {
752 let read = reader.read(&mut chunk)?;
753 if read == 0 {
754 return Ok((bytes, exceeded));
755 }
756 let remaining = max_output_bytes.saturating_sub(bytes.len());
757 if remaining >= read && !exceeded {
758 bytes.extend_from_slice(&chunk[..read]);
759 } else {
760 exceeded = true;
761 if remaining > 0 {
762 bytes.extend_from_slice(&chunk[..remaining]);
763 }
764 }
765 }
766}
767
768#[cfg(test)]
769#[path = "python_tests.rs"]
770mod tests;