Metadata-Version: 2.5
Name: py-runner-kernel
Version: 0.1.2
Summary: Persistent Python code runner: string in, JSON out. A lightweight, dependency-free ipykernel-like execution library.
Author-email: RightFix <righteousnessude@gmail.com>
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Keywords: code-execution,data-science,jupyter,kernel,repl
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Interpreters
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# py-runner

[![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
[![Python >=3.10](https://img.shields.io/badge/python-%3E%3D3.10-blue.svg)](pyproject.toml)
[![TestPyPI](https://img.shields.io/badge/TestPyPI-py--runner-green.svg)](https://test.pypi.org/project/py-runner/)
[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://rightfix.github.io/py-runner-docs/)

Persistent Python code runner: **string in, JSON out**. A lightweight,
dependency-free, ipykernel-like execution library — no Jupyter server, no ZMQ,
no required third-party packages.

📖 **Full documentation: https://rightfix.github.io/py-runner-docs/**

Persistent Python code runner: **string in, JSON out**. A lightweight,
dependency-free, ipykernel-like execution library — no Jupyter server, no ZMQ,
no required third-party packages.

```python
from py_runner import Kernel

k = Kernel()
k.execute("x = 40 + 2")
print(k.execute("x + 1"))
# {"output": null,
#  "display": [{"type": "text/plain", "data": "43"}],
#  "error": null, "execution_count": 2, "execution_time": 0.0001}
```

## Install

```bash
pip install py-runner-kernel
```

Requires Python >= 3.10. Zero runtime dependencies.

## Quickstart

```python
from py_runner import Kernel, execute, execute_json

# Module-level shared kernel
execute("import math")
print(execute_json("math.sqrt(16)"))

# Isolated session kernel (one per user/request)
k = Kernel(on_input=lambda prompt: "bob", max_history=100)
r = k.execute('name = input("who? ")\nname.upper()')
assert r["display"][0]["data"] == "'BOB'"
```

Each result is a JSON-serializable dict:

| key | value |
|---|---|
| `output` | captured stdout (`str \| None`) |
| `display` | rich values (`[{type, data}]`: `text/plain`, `text/html`, `image/png` base64, …) |
| `error` | traceback / warning text (`str \| None`) |
| `execution_count` | incrementing cell number |
| `execution_time` | seconds (float) |

## Features

- **Persistent namespace** with Jupyter-style `_`, `__`, `___` and bounded `Out[N]` history
- **Last-expression display** — DataFrames as HTML, arrays/tensors summarized, matplotlib figures as PNG, trailing `;` suppresses output
- **Magics**: `%time`, `%timeit`, `%who`/`%whos`, `%run`, `%pip`, `!shell`, `%reset`
- **Long runs**: `timeout=`, cooperative `interrupt()`, `on_stream` chunks, `on_heartbeat` for silent `fit()` loops, tqdm `\r` collapsing, `logging`/`warnings` capture
- **Sandboxing**: `allow_shell=False`, `allow_pip=False`, `set_address_space_limit(mb)` (Unix), `reset(keep_imports=True)`
- **CLI**: `py-runner -c "1+1"`, `py-runner script.py`, `... | py-runner`, with `--timeout`, `--no-shell`, `--no-pip`

## CLI

```bash
py-runner -c "print(2 + 3)"
py-runner analysis.py --timeout 300 > result.json
cat cell.py | py-runner
python -m py_runner -c "1+1"
```

## Project structure

```text
py_runner/
├── __init__.py   # public API: Kernel, execute, execute_json, reset, main
├── kernel.py     # persistent execution kernel (threaded, timeouts, input)
├── magics.py     # %time, %pip, !shell, … (sandbox-flaggable)
├── display.py    # rich display collector (pandas/numpy/PIL/matplotlib)
├── streams.py    # thread-routed streaming stdout, tqdm \r handling
└── utils.py      # guarded imports, address-space limit helper
```

## Development

```bash
uv sync            # create .venv
uv run python -c "from py_runner import execute; print(execute('1+1'))"
uv build           # wheel + sdist in dist/
uv publish --publish-url https://test.pypi.org/legacy/  # trial run
```

## HTTP server

```bash
py-runner-serve --host 127.0.0.1 --port 8000 --max-sessions 100
```

```bash
S=$(curl -s -X POST localhost:8000/sessions | python3 -c "import sys,json; print(json.load(sys.stdin)['session_id'])")
curl -s -X POST localhost:8000/execute \
  -d "{\"session_id\":\"$S\",\"code\":\"x = 40 + 2\"}"
curl -s -X POST localhost:8000/execute \
  -d "{\"session_id\":\"$S\",\"code\":\"x + 1\"}"        # display: 43 (state kept)
curl -s -X POST localhost:8000/interrupt \
  -d "{\"session_id\":\"$S\"}"
curl -s localhost:8000/sessions                          # list live sessions
curl -s -X DELETE localhost:8000/sessions/$S             # destroy session
```

Sessions are server-generated UUIDs, live until deleted (no idle timeout),
and isolated per ID. Kernels default to `allow_shell=false, allow_pip=false`;
pass `{"allow_shell": true}` to `POST /sessions` to relax per session.

## License

AGPL-3.0-or-later — see [LICENSE](LICENSE). Note the network clause
(§13): if you serve a modified version over a network, you must offer its
source to those users.
