python_script_runner 1.7.10

Execute Python scripts from Rust with path traversal prevention and environment isolation
Documentation
# PYTHON_SCRIPT_RUNNER

Execute Python scripts from Rust with automatic project-relative path resolution, retry logic, and real-time output streaming.

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
python_script_runner = "0.1"
```

Or use local path:
```toml
python_script_runner = { path = "path/to/python_script_runner" }
```

## Quick Start

### Set environment variables in .env
```bash
PROJECT_DIRECTORY=/path/to/your/project
```

### Import and use
```rust
use python_script_runner::python_script_runner::{PythonExecutor, run_python_script};

let executor = PythonExecutor::new();
executor.execute_script("scripts/my_script.py").await?;

run_python_script("/absolute/path/to/script.py").await?;
```

## Configuration

### Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `PROJECT_DIRECTORY` | Yes || Root directory for script path resolution |

## Key Features

1. **Automated Python Script Execution** — Runs Python scripts via `tokio::process::Command` with async output capture — non-blocking
2. **Robust Retry Mechanism** — 3-attempt retry loop with fixed 3-second delay between attempts — covers transient failures (interpreter startup, temp file locks)
3. **Real-Time Output Streaming** — Captures and writes both stdout and stderr via `tokio::io` async writers — operator sees script progress immediately
4. **Project-Aware Path Management** — Resolves relative paths using `PROJECT_DIRECTORY` environment variable — portable across machines and CI
5. **Early File Validation** — Pre-validates script existence before entering the retry loop — fails fast rather than waiting 9 seconds for a missing file

## API Reference

### PythonExecutor::new() -> Result<PythonExecutor>
Create a new executor, loading `PROJECT_DIRECTORY` from `.env`.
- Returns: `Ok(PythonExecutor)` on success

### PythonExecutor::execute_script(&self, relative_path: &str) -> Result<()>
Execute a Python script given a project-relative path.
- `relative_path`: Path relative to `PROJECT_DIRECTORY` (e.g. "scripts/my_script.py")
- Returns: `Ok(())` on success

### run_python_script(script_path: &str) -> Result<()>
Execute a Python script at the given absolute path with retry and output streaming.
- `script_path`: Absolute path to the Python script
- Returns: `Ok(())` on success

## Output

Both stdout and stderr are streamed to the respective streams in real-time:
- stdout: Written to the executor's stdout
- stderr: Written to the executor's stderr

The script exit code is captured and returned as part of the `Result`.

## Error Handling

The library returns `anyhow::Result<T>`:
- `anyhow::anyhow!("PROJECT_DIRECTORY not set")` if not set
- `anyhow::anyhow!("Script file not found")` if the script doesn't exist
- `anyhow::anyhow!("Python interpreter not found")` if `python` or `python3` is not available
- `anyhow::anyhow!("Script execution failed with exit code N")` if the script fails

## Retry Policy

Script execution uses a 3-attempt retry loop:
- **Max attempts**: 3
- **Delay**: Fixed 3-second delay between attempts
- **Retryable**: All execution failures
- **Not retried**: Missing script file (fail fast)

**Note:** Performance is dominated by the external Python interpreter and script execution time. In-crate benchmarks are not meaningful.

## Test Coverage

```bash
cargo test --lib
```

| Function | Tier | Tests | What is tested |
|----------|------|-------|----------------|
| `PythonExecutor::new()` | 1 | 3 | success, missing PROJECT_DIRECTORY, missing .env |
| `execute_script()` | 1 | 4 | relative path, absolute path, retry on failure, output streaming |
| `run_python_script()` | 1 | 3 | success, missing script, python not found |
| Path resolution | 2 | 2 | relative path construction, fallback |

**Note:** Tests that require a Python interpreter will skip gracefully if `python`/`python3` is not available on the system PATH.

## Dependencies

```toml
[dependencies]
dotenvy = "0.15"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
```