Skip to content

Talking To The World

Programs eventually need to speak to something outside themselves — a file, a subprocess, a socket, a supervised service. Aura exposes that surface through four built-in modules: io for standard streams, fs for files and directories, process for subprocesses and supervisors, and net for sockets, HTTP, and WebSockets.

The APIs in these modules share a shape. Operations that can fail return Result. Resources cleaned up by the runtime are meant to live inside a with block. Waits that might block indefinitely accept a timeout argument and tell the caller explicitly when that timeout fires. Everything works together with match, try, with, and TaskGroup.

Files: Read, Parse, Report

The simplest filesystem API is one-shot:

Aura
import fs

path = "tmp.txt"
try fs.write_string(path, "limit=42\n")

match fs.read_to_string(path):
    case Result.Ok(text):
        print(text.trim())
    case Result.Err(error):
        print(error)

One-shot fs.read_to_string and fs.read_bytes are capped at 256 MiB. The same cap applies to the remaining contents read by fs.File.read_all() and fs.File.read_bytes(). An accidental whole-file read against a very large log fails at the cap. Larger files need a host helper or pre-splitting because Aura 0.2 has no incremental file-read member.

Aura
import fs
import io

def copy_text(source: str, dest: str) -> Result[None, io.Error]:
    with input = try fs.open(source):
        text = try input.read_all()

    with output = try fs.create(dest):
        try output.write_all(text)
        try output.flush()

    return Result.Ok(None)

fs.File is a resource. Put it in a with block and cleanup is the compiler's problem, not yours. The with ends automatically on both normal and error paths.

Standard Streams

print(value) renders a value and adds a newline. When a program needs more control — writing without a newline, flushing for a prompt, reading a line from standard input — the io module has it:

Aura
import io

try io.write("name> ")
try io.flush()

match io.read_line():
    case Result.Ok(Option.Some(line)):
        print("hello " + line.trim())
    case Result.Ok(Option.None):
        print("end of input")
    case Result.Err(error):
        print(error)

io.read_line() returns Result[Option[str], io.Error]. The Option is None at end of input; the Result captures I/O failures. Both are in the type, and a caller that wants to treat them differently can.

Processes: No Shell By Default

process.run executes a subprocess from an argument list. There is no shell interpretation, so the arguments are not re-split and there are no quoting hazards. The return value is a process.Completed record.

Aura
import process

completed = try process.run(command=["/bin/echo", "aura process"], stdout=process.pipe(), stderr=process.pipe(), timeout=1s, group=true)

try completed.check()
print(completed.stdout().trim())

Two things in that call site are worth explaining. stdout=process.pipe() captures the subprocess's output so the parent can read it; stderr=process.pipe() does the same for standard error. group=true places the child in its own process group on Unix hosts, so termination reaches the leader and all descendants.

Omitting timeout supplies no caller deadline through an internal absence marker. An explicit negative Duration is not that marker: invalid timeout or deadline values return process.Error.Io(io.Error.InvalidInput).

When a child writes bytes that are not valid UTF-8, use stdout_bytes() and stderr_bytes():

Aura
bytes = completed.stdout_bytes()
print(bytes.len())

Interacting With A Child

process.start returns a process.Child you can talk to while the child is running:

Aura
import process

child = try process.start(command=["/bin/cat"], stdin=process.pipe(), stdout=process.pipe(), stderr=process.pipe(), group=true)

match child.stdin():
    case Option.Some(pipe):
        try pipe.write_all("hello\n")
        pipe.close()
    case Option.None:
        print("stdin was not piped")

match child.stdout():
    case Option.Some(pipe):
        text = try pipe.read_all()
        print(text.trim())
    case Option.None:
        print("stdout was not piped")

match child.wait(timeout=1s):
    case process.Wait.Exited(status):
        print(status)
    case process.Wait.TimedOut:
        child.kill()
    case process.Wait.Cancelled:
        child.terminate()
    case process.Wait.Failed(error):
        print(error)

child.close()

child.stdin(), child.stdout(), and child.stderr() return Option[process.Pipe] so the program can tell the difference between "the stream was not piped" and "the stream is available."

Supervisors

When a program needs to manage several named subprocesses — start them, observe their lifetimes, restart them according to a policy — use a process.supervisor:

Aura
import process

with supervisor = process.supervisor():
    try supervisor.start(name="worker", command=["/bin/sleep", "1"], restart=process.RestartPolicy.Never, group=true)

    match supervisor.wait(timeout=2s):
        case process.SupervisorWait.Event(event):
            print(event)
        case process.SupervisorWait.TimedOut:
            print("no event")
        case process.SupervisorWait.Cancelled:
            print("cancelled")

Supervisor names are unique within a supervisor. Starting a second child with the same name returns an error and preserves the existing child. Leaving the with block stops every child the supervisor still manages.

Networking: TCP

Network APIs return Result[..., io.Error]. Waits accept timeout=.... Listeners, streams, and other resources belong in with blocks.

Aura
import net

with listener = try net.listen("127.0.0.1:0"):
    address = try listener.local_addr()

    with stream = try net.connect_timeout(address, timeout=1s):
        try stream.write_all("ping\n", timeout=1s)
        try stream.shutdown_write()

Hostname lookup and blocking connect syscalls are sent to the generic blocking-I/O pool, so they do not freeze sibling Aura tasks. The 1s timeout above is one shared budget for queue admission, DNS, and every candidate address. Task-group cancellation stops waiting promptly. Before pool acceptance it prevents submission; after acceptance, the host resolver cannot generally be interrupted and its eventual result is discarded.

Operators may set AURA_BLOCKING_WORKERS to an exact positive worker count and AURA_BLOCKING_QUEUE_CAPACITY to a positive bound on accepted pending jobs. The absent worker setting derives 2..=8 workers from host parallelism, with fallback 4; the absent queue setting is unbounded. Full-queue admission is FIFO and scheduler-aware. A queue bound limits accepted pending backlog, not admission waiters, and cannot guarantee unrelated blocking-I/O progress while every worker remains stuck.

A live listener or stream is not Transfer, so it cannot be captured by a new task. The task that creates a listener keeps it and its accepted streams; it may use an ordinary helper on that same task to process a connection:

Aura
import io
import net

def handle(stream: own net.TcpStream) -> Result[None, io.Error]:
    with conn = stream:
        line = try conn.read_line(timeout=5s)
        match line:
            case Option.Some(text):
                try conn.write_all(text, timeout=5s)
            case Option.None:
                pass
    return Result.Ok(None)

When a server itself should run as a child task, let that child create the listener. A copy Queue[str] handle can cross the boundary so the child can publish its bound address to the parent; the live listener never leaves its owning task.

The read_line returns Result[Option[str], io.Error] for the same reason io.read_line does: the client might close cleanly, and the program might have to decide what that means.

HTTP And WebSockets

HTTP client helpers return net.HttpResponse:

Aura
import net

headers: dict[str, str] = {}
response = try net.http_request_text_timeout(method="GET", url="http://127.0.0.1:8080/", body="", headers=headers, timeout=2s)

print(response.status())

HTTP servers use net.http_listen to create an HttpListener; accepting a connection returns an HttpExchange carrying request data and the methods to send a response.

WebSocket APIs follow the same resource style: create or accept a socket, send and receive text or bytes, then close. See Network Module for the full surface.

The Common Shape

Most system-facing Aura code has the same outline:

Aura
import fs
import io

def load(path: str) -> Result[str, io.Error]:
    with file = try fs.open(path):
        text = try file.read_all()
        return Result.Ok(text)
  • import the module.
  • Call an API that returns Result.
  • Use try when the caller should receive the failure.
  • Use match when the current function makes a decision.
  • Put resources in with.
  • Pass a timeout to any wait that should not block forever.

Reference: Filesystem Module, Process Module, Network Module, I/O Module.

Aura 0.3.2 technical preview. Implementation baseline: 837eb9756ed9efdca275d960edf12317fff1aa9c.