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
# 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
| Shows | Code sent | Arguments | Status | Response |
|---|---|---|---|---|
| code that defines run | | {"name": "owt"} | 200 | hello, owt |
| an expression becomes run | | {"word": "stressed"} | 200 | desserts |
| a chain of calls is appended to pipe() | | {"text": "quiet please"} | 200 | QUIET 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.
| Shows | Code sent | Arguments | Status | Response |
|---|---|---|---|---|
| server globals persist between requests | | | 200 | 1 |
| the same request again | | | 200 | 2 |
| a module-level import is not visible inside run | | | 500 | Error executing Unsafe code: Error executing Unsafe code: name 'time' is not defined |
| an exception is a 500 | | | 500 | Error executing Unsafe code: Error executing Unsafe code: nope |
| cached by path | | {"use_cache": true} | 200 | 1792283790056393351 |
| the same path again: the cached answer | | {"use_cache": true} | 200 | 1792283790059351956 |
- 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-levelimportlands in the locals, which a function body cannot see. Importing inside the function works. - The cache never engages. With
use_cacheset, the same path should return the stored answer, but two calls return different timestamps: the firstcasein the cache-keymatchis 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:
| Shows | Code sent | Arguments | Status | Response |
|---|---|---|---|---|
| no credentials | | | 401 | Unauthorized Access |
| with credentials | | | 200 | hi |
The test suite: 28 passed in 0.42s.