owt

A small Python server whose endpoints are defined by the requests sent to them. Each request carries code and arguments, base64-encoded; the server runs the code and returns what it produces. Any program that can make an HTTP request can call a Python library through it, with no endpoint written in advance.

The name is northern English for "anything", as in its tagline: serve up owt yer fancy on t'fly.

It runs whatever it is sent. That is the point, and its README says so plainly: don't expose it to the internet. It listens on all interfaces by default, with no authentication unless you pass --auth, so start it with --address 127.0.0.1 unless you mean otherwise.

A request

What happens to a request How owt handles a request. Any path is accepted, by GET with query parameters or by POST with a JSON body, behind optional basic authentication. The body carries code and keyword arguments, both base64-encoded. The server tries the code three ways until one defines run: as written; as an expression assigned to run; and as a chain of calls appended to pipe(). It executes the code with the server module's own globals, calls run with the decoded arguments, and turns the result into the HTTP response. any clientcurl, Python, Haskell /any/pathGET or POST · basic auth decodecode_b64 · kwargs_b64 find runas written · as an expression· appended to pipe() execwith the server's globals responserun(**kwargs)
The path is only a name. Every path reaches the same handler; what an endpoint does is entirely in the body.
# the JSON body of a POST
{
  "code_b64": base64(CODE),
  "kwargs_b64": base64("{'name': 'owt'}"),
  "fn_name": "run",          # optional
  "use_cache": false         # optional
}

The bundled client does the encoding, and there is a Haskell one in owt-hs:

python -m owt.server --address 127.0.0.1
python -m owt.client --code "lambda word: word[::-1]" \
    --arg word '"stressed"' --address http://127.0.0.1:9876/owt
desserts
The three ways code can define run, as sent to a local server
ShowsCode sentArgumentsStatusResponse
code that defines run
def run(name):
    return f"hello, {name}"
{"name": "owt"}200hello, owt
an expression becomes run
lambda word: word[::-1]
{"word": "stressed"}200desserts
a chain of calls is appended to pipe()
kwarg('text').f(str.upper)
{"text": "quiet please"}200QUIET PLEASE

The pipe syntax

pipe() starts a chain of small adaptors, each taking the previous step's output. kwarg('text') picks an argument, f(fn) applies a function, open(root_dir=…) opens a file, bytes() reads it, and there are map, foldl, cond, fork and indexing for the rest.

Some steps reach outside Python on purpose: shell() runs its input as a shell command, importing(…) imports modules for the rest of the chain, and installing(…) runs pip install on the server. That is how the repository's adaptors in owt/lib put text-to-speech models such as Bark, MeloTTS, Orpheus, Parler and XTTS behind a URL.

# a file, read and upper-cased (from the README)
run = (pipe()
       .kwarg('path')
       .open(root_dir='./example/summat')
       .bytes()
       .f(lambda ab: ab.decode('utf-8').upper())
       .done())

Sharp edges

Each row below is a real request to a local server, recorded by scripts/site/capture.py, which only ever binds to 127.0.0.1.

Sharp edges, as sent to a local server
ShowsCode sentArgumentsStatusResponse
server globals persist between requests
def run():
    global hits
    hits = globals().get('hits', 0) + 1
    return str(hits)
2001
the same request again
def run():
    global hits
    hits = globals().get('hits', 0) + 1
    return str(hits)
2002
a module-level import is not visible inside run
import time
def run():
    return str(time.time_ns())
500Error executing Unsafe code: Error executing Unsafe code: name 'time' is not defined
an exception is a 500
def run():
    raise ValueError("nope")
500Error executing Unsafe code: Error executing Unsafe code: nope
cached by path
def run():
    import time
    return str(time.time_ns())
{"use_cache": true}2001792283790056393351
the same path again: the cached answer
def run():
    import time
    return str(time.time_ns())
{"use_cache": true}2001792283790059351956
  • State persists, and so can damage. Code runs with the server module's own globals, so a request can leave values behind for later ones, as the counter shows, and can equally replace the server's own functions.
  • Imports at the top of the code are not visible inside run. The code is executed with separate global and local dictionaries; a module-level import lands in the locals, which a function body cannot see. Importing inside the function works.
  • The cache never engages. With use_cache set, the same path should return the stored answer, but two calls return different timestamps: the first case in the cache-key match is a capture pattern that always matches, so the key is always the override, normally empty.
  • Errors are reported twice over. An exception is wrapped once when it is raised and again by the handler, so messages begin "Error executing Unsafe code: Error executing Unsafe code".

Authentication

--auth user:sha256(password) turns on HTTP basic authentication for every path:

With --auth
ShowsCode sentArgumentsStatusResponse
no credentials
lambda: 'hi'
401Unauthorized Access
with credentials
lambda: 'hi'
200hi

The test suite: 28 passed in 0.42s.