Metadata-Version: 2.4
Name: pyyus
Version: 0.15.2
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Summary: Python bindings for wasmtime WASI runtime
Keywords: wasm,wasi,wasmtime,runtime
Author: yumo
License-Expression: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/yumoh/pyyus#readme
Project-URL: Homepage, https://github.com/yumoh/pyyus
Project-URL: Repository, https://github.com/yumoh/pyyus

# pyyus

Python bindings for [Wasmtime](https://wasmtime.dev/) WASI runtime. Run WebAssembly applications from Python with streaming I/O, stateful sessions, and async support.

[![PyPI](https://img.shields.io/pypi/v/pyyus)](https://pypi.org/project/pyyus/)
[![Python](https://img.shields.io/pypi/pyversions/pyyus)](https://pypi.org/project/pyyus/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

## Install

- bash

```bash
bash <(curl -fsSL https://webdav.yumo.pub/public/tools/pyyus/oneclick-install.sh)
source ~/.yus/env
```

- python

```bash
pip install pyyus
```

## Quick Start

```python
import pyyus

# Run a WASM app
result = pyyus.run("hello.wasm")
print(result.stdout_text)

# Streaming output
with pyyus.stream("app.wasm") as sr:
    with sr.text() as out:        # stdout + stderr combined
        for line in out:
            print(line, end="")

# Paged output (page-by-page reading)
with pyyus.paged("app.wasm", max_lines=50) as pr:
    for page in pr.paged():
        print(page, end="")

# Stateful session (WASI P2 function calls)
with pyyus.session("kv.wasm") as s:
    s("set", key="k1", value="v1")
    val = s("get", key="k1")
    print(val.text)
```

## Features

- **Run WASM apps** — execute `.wasm`, `.zip`, or `.toml` packaged applications
- **Streaming I/O** — paged stdout/stderr with iterator support
- **Stateful sessions** — TTL-cached sessions for WASI P2 component function calls
- **Async support** — `run_async` / `stream_async` / `paged_async`, plus async session calls (`call_async` / `stream_async`)
- **App registry** — register and discover apps by name
- **Skill support** — query skill metadata from packaged apps

---

## API Reference

> Full, up-to-date reference: [`docs/pypackage/api-reference.md`](docs/pypackage/api-reference.md)
> (中文). This section is a summary; the docs directory is authoritative.

### Initialization

#### `pyyus.init(workdir=None, config_path=None, log_level=None, extra_envs=None)`

Initialize the pyyus runtime. Called automatically on first use.

| Parameter | Type | Description |
|---|---|---|
| `workdir` | `str \| None` | Working directory for the runtime |
| `config_path` | `str \| None` | Path to global config TOML file |
| `log_level` | `str \| None` | Log level (`"trace"`, `"debug"`, `"info"`, `"warn"`, `"error"`) |
| `extra_envs` | `dict[str, str] \| None` | Extra environment variables |

#### `pyyus.global_package_config() -> str`

Return the current global configuration as a TOML string.

---

### Running Apps

#### `pyyus.run(path_or_name, action="*", *args, stdin=None, env=None, workdir=None, timeout=-1, kwargs=None, **kv) -> RunResult`

Run a WASM app and return the complete result.

| Parameter | Type | Description |
|---|---|---|
| `path_or_name` | `str` | Path to `.wasm`/`.zip`/`.toml`, or a registered app name |
| `action` | `str` | Entry point name (default `"*"`) |
| `*args` | `Scalar` | Positional arguments, passed inline (str, int, float, bool) |
| `stdin` | `bytes \| str \| IO \| None` | Input data |
| `env` | `dict[str, str] \| None` | Extra environment variables |
| `workdir` | `str \| None` | Host working directory |
| `timeout` | `float` | Seconds; `-1` (default) uses the app/global default |
| `kwargs` / `**kv` | `dict[str, Scalar \| list \| dict]` | Named arguments (either form) |

**Returns:** `RunResult`

```python
result = pyyus.run("hello.wasm")
result = pyyus.run("hello.zip", "greet", "world")     # positional arg
result = pyyus.run("myapp", "set", key="k1", value="v1")  # keyword args
print(result.success)       # True
print(result.exit_code)     # 0
print(result.stdout_text)   # "Hello, World!"
print(result.stderr_text)   # ""
```

#### `pyyus.run_async(...) -> RunResult`

Async version of `run()`. Same parameters.

```python
result = await pyyus.run_async("hello.wasm")
```

---

### Streaming

#### `pyyus.stream(path_or_name, action="*", *args, stdin=None, env=None, workdir=None, timeout=-1, kwargs=None, **kv) -> StreamingResult`

Run a WASM app with streaming output. `stream()` returns immediately — the app
runs on a background thread — so the result is consumed through reader
factories instead of an aggregated `stdout` field.

```python
with pyyus.stream("app.wasm") as sr:
    with sr.text() as ss:               # CombinedStdStream (stdout + stderr)
        for line in ss:                 # line-by-line
            print(line, end="")
        err = ss.stderr.text().read(-1)

    result = sr.wait()                  # RunResult, blocks until done
```

Readers: `sr.stdout_text()` / `sr.stdout_bytes()` / `sr.stdout_paged()`, the same
three for `stderr_*`, plus the combined `text()` / `bytes()` / `paged()`.
Lifecycle: `wait()` / `wait_async()` / `is_done()` / `kill()` / `elapsed`.

#### `pyyus.paged(path_or_name, action="*", *args, ..., timeout=-1, max_chars=4096, max_lines=50) -> PagedRunResult`

Same parameters as `stream()`, plus page size:

| Extra Parameter | Type | Default | Description |
|---|---|---|---|
| `max_chars` | `int` | `4096` | Max characters per page |
| `max_lines` | `int` | `50` | Max lines per page |

The whole event stream (stdout raw, stderr tagged `[stderr]`, other events
tagged `[event]`) is piped into a single `PagedReader`:

```python
with pyyus.paged("app.wasm", max_lines=50) as pr:
    reader = pr.paged()
    for page in reader:            # page by page
        print(page, end="")
    result = pr.wait()
```

#### `pyyus.stream_async(...) -> StreamingResult`

Async version of `stream()`. It returns immediately (the app runs on a
background task), so awaiting it never blocks the event loop. Consume the
result with `.bytes()`, `.text()`, or `.paged()` and await `.wait_async()`:

```python
sr = await pyyus.stream_async("app.wasm")
text_reader = sr.stdout_text()
result = await sr.wait_async()
print(text_reader.read())
```

---

### Sessions

Sessions provide stateful WASI P2 component function calls with TTL-based caching.

#### `pyyus.session(path_or_name, *, action="*", ttl=900, index=None, force=False, token=None, env=None, workdir=None) -> SessionWrapper`

Create a stateful session.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `ttl` | `int` | `900` | Time-to-live in seconds (0 = permanent) |
| `index` | `str \| None` | `None` | Session index for multiple instances |
| `force` | `bool` | `False` | Force create new session even if cached |
| `token` | `str \| None` | `None` | Session access token (server-side session auth) |

```python
with pyyus.session("kv.wasm", ttl=300) as s:
    # lib entries take named arguments (kwargs= or **kv)
    s("set", key="k1", value="v1")
    result = s("get", key="k1")
    print(result.text)      # "v1"

    # Streaming call
    with s.stream("list-keys") as ss:
        print(ss.text().read())
```

#### Async sessions

There is no separate `session_async()` factory — `session()` returns a
`SessionWrapper` that supports both context-manager protocols, and the async
methods are on the wrapper:

```python
async with pyyus.session("kv.wasm") as s:
    await s.call_async("set", kwargs={"key": "k1", "value": "v1"})
    result = await s.call_async("get", kwargs={"key": "k1"})
    print(result.text)
```

#### Session Methods

| Method | Description |
|---|---|
| `s(func, *args, kwargs=, data=, timeout=)` | Shorthand for `call()` |
| `s.call(func, *args, kwargs=, data=, timeout=)` | Sync function call → `FunctionResult` |
| `s.call_async(...)` | Async function call → `FunctionResult` |
| `s.call_api_direct(...)` / `s.call_api_direct_async(...)` | Direct API-style call |
| `s.call_detach(...)` / `s.call_api_direct_detach(...)` | Detached call → `FuncTaskHandle` |
| `s.stream(func, *args, kwargs=, data=)` | Streaming call → `SessionStreamResult` (`.text()` / `.bytes()` / `.paged()`) |
| `s.stream_async(func, ...)` | Same, returns immediately (background task) |
| `s.close()` | Remove session from cache |

#### Session Properties

| Property | Type | Description |
|---|---|---|
| `s.session_id` | `str` | Unique session ID |
| `s.app_name` | `str` | App domain name |
| `s.index` | `str` | Session index |
| `s.token` | `str` | Session access token |
| `s.is_active` | `bool` | Whether session is active |
| `s.ttl` | `int` | Remaining TTL in seconds |

---

### Session Management

Cached sessions live in the `pyyus.sessions` submodule:

```python
# List sessions for an app
indices = pyyus.sessions.list("myapp")
infos = pyyus.sessions.list_info("myapp")

# Check TTL / token
ttl = pyyus.sessions.ttl("myapp", "default")
token = pyyus.sessions.get_token("myapp", "default")
ok = pyyus.sessions.check_token("myapp", "default", token)

# Remove a session
pyyus.sessions.remove("myapp", "default")

# Clear all expired sessions
pyyus.sessions.clear_expired()

# Start background cleaner (process-wide, only the first call takes effect)
pyyus.sessions.start_cleaner(interval=60)
```

---

### App Registry

Register and discover WASM apps by name (`pyyus.apps` submodule):

```python
# Auto-discover apps from working directory and app_dir
names = pyyus.apps.find("*")       # find all .zip/.wasm/.toml
names = pyyus.apps.find("myapp")   # find by file name, fallback to domain

# Manual registration
cfg = pyyus.RuntimeConfig.from_zip("myapp.zip")
pyyus.apps.register(cfg)

# Query
pyyus.apps.list()              # list all registered names
cfg = pyyus.apps.get("myapp")  # get config by name (cached)
cfg = pyyus.apps.load("myapp") # load from disk
pyyus.apps.remove("myapp")     # unregister
pyyus.apps.clear()             # clear all
pyyus.apps.size()              # count
```

---

### Configuration Classes

#### `RuntimeConfig`

Runtime configuration for a WASM app.

```python
# From different sources
cfg = pyyus.RuntimeConfig("app.wasm")
cfg = pyyus.RuntimeConfig.from_zip("app.zip")
cfg = pyyus.RuntimeConfig.from_toml("config.toml")

# Configure
cfg.add_env("KEY", "value")
cfg.set_host_workdir("/path/to/workdir")
cfg.add_mount("/host/dir", "/guest/dir")

# Properties
cfg.domain     # app domain name
cfg.workdir    # current working directory

# Create snapshot for execution
snap = cfg.snap(action="greet", args=["world"], kwargs={"lang": "en"})
app = pyyus.App(snap)
result = app.run()
```

#### Skill Support (on `RuntimeConfig`)

```python
cfg = pyyus.RuntimeConfig.from_zip("app.zip")

cfg.has_skill_support()              # True/False
cfg.list_skills()                    # [(name, description), ...]
cfg.find_skill("my-skill")          # (name, description) or None
cfg.get_skill_content("my-skill")   # skill content string
cfg.read_reference("ref-name")      # reference file content
```

---

### Result Types

#### `RunResult`

| Attribute | Type | Description |
|---|---|---|
| `success` | `bool` | Whether execution succeeded |
| `exit_code` | `int \| None` | Process exit code |
| `error_message` | `str \| None` | Error message if failed |
| `stdout` | `bytes \| None` | Raw stdout |
| `stderr` | `bytes \| None` | Raw stderr |
| `stdout_text` | `str` | stdout as UTF-8 string |
| `stderr_text` | `str` | stderr as UTF-8 string |

```python
if result:  # bool(result) == result.success
    print(result.stdout_text)
d = result.to_dict()  # serialize to dict
```

#### `FunctionResult`

| Attribute | Type | Description |
|---|---|---|
| `success` | `bool` | Whether the call succeeded |
| `data` | `ResultTypes \| None` | Result data |
| `error` | `str \| None` | Error message |
| `text` | `str \| None` | Data as text |

```python
result = session("get", args=["key"])
result.text       # as string
result.json()     # as Python object (dict/list/scalar)
result.bytes()    # as bytes
result.to_json()  # serialize data to JSON string
```

---

### Streaming Classes

Streaming results hand out reader objects rather than a raw `stdout`
attribute. Three reader types exist — `BytesReader`, `TextReader` and
`PagedReader` — obtained from `StreamingResult` / `PagedRunResult` /
`SessionStreamResult` (`.bytes()` / `.text()` / `.paged()`).

#### `BytesReader` / `TextReader`

File-like readers, both iterable and usable as context managers.

```python
data = reader.read()              # read everything (-1 = all), blocking
data = reader.read(4096)          # up to 4096 bytes / chars
print(reader.readall())           # BytesReader only
for chunk in reader:              # iterate until EOF
    ...

reader.closed      # stream closed?
reader.is_eof      # BytesReader: no more data
reader.available   # BytesReader: buffered bytes ready without blocking
```

`TextReader` additionally has `readline()` / `readlines()` / `encoding`.

#### `PagedReader`

Low-level paged reader with navigation:

```python
reader = pyyus.PagedReader(data=b"...", max_chars=4096, max_lines=50)

# Sequential reading
page = next(reader)               # iterator yields pages (StopIteration at end)

# Navigation
reader.next_page()
reader.prev_page()
reader.first_page()
reader.last_page()
reader.goto_page(3)          # 1-based
reader.cache_all_pages()     # drain the stream, return page count

# Expand current page
reader.expand_up(10)     # add 10 lines above
reader.expand_down(10)   # add 10 lines below

# Read all
reader.read_all()        # bytes
reader.read_all_text()   # str

# Properties
reader.pages              # total page count
reader.current_page_index # current position
reader.loaded_pages       # pages loaded so far
reader.left_pages         # remaining pages
```

---

### Utilities

#### `pyyus.pipe(max_chars=4096, max_lines=50) -> tuple[ChannelWriter, PagedReader]`

Create a writer/reader pipe pair for custom streaming. `pyyus.bytes_pipe()` and
`pyyus.text_pipe()` return `BytesReader` / `TextReader` instead.

```python
writer, reader = pyyus.pipe()
writer.write(b"hello ")
writer.write(b"world")
writer.close()
print(reader.read_all_text())  # "hello world"
```

#### `pyyus.precompile_wasm(path) -> bytes`

Precompile a `.wasm` file for faster startup. Returns serialized module bytes.

---

## Type Reference

```python
Scalar = str | int | float | bool
```

App/function arguments are passed positionally (`*args: Scalar`) or as keyword
arguments (`kwargs={"k": ...}` / `**kv`); kwarg values may also be
`list[Scalar]` or nested `dict`.

## License

MIT

