Docs · Reference

SDKs and OpenAPI

The whole API is described in froogcorp.com/openapi.json (OpenAPI 3.1): generate a client in any language from it. Three ready clients with no dependencies, generated from that document, each one file from froogcorp.com. TypeScript is also the npm package froogdev:

# Python 3.8+, standard library only: one file, imported as froogdev
$ curl -fsSL https://froogcorp.com/sdk/froog.py -o froogdev.py
# TypeScript: Node 18+, Deno, Bun or a browser (uses fetch)
$ npm install froogdev
# Go 1.21+, standard library only: put it in a package directory of your module (package froog)
$ mkdir -p froog && curl -fsSL https://froogcorp.com/sdk/froog.go -o froog/froog.go

The files are froogcorp.com/sdk/froog.py (save it as froogdev.py), froog.ts (import it as "./froog", or use the npm package) and froog.go. Download them again to update; the npm package's version is the API version.

A sandbox is an object you act on by method. Creating one waits until it's running. Leaving the with block (Python), the await using scope (TypeScript) or calling Delete (Go) deletes it, unless you create it with keep=True.

# Python
from froogdev import Froog
froog = Froog()                                   # FROOG_API_KEY from the environment
with froog.sandboxes.create(size="small", env={"TENANT_ID": "42"}) as sb:
    r = sb.exec("uname -a", cwd="/tmp", timeout=60)
    print(r.stdout, r.exit_code)
    sb.files.write("app.py", "print('hi')")
    print(sb.exec("python3 app.py", check=True).stdout)   # check=True raises CommandError on a non-zero exit
    for e in sb.exec_stream("make test"):              # output as it arrives
        print(e.get("data", ""), end="")

// TypeScript
import { Froog } from "froogdev";
const froog = new Froog({ apiKey: process.env.FROOG_API_KEY });
await using sb = await froog.sandboxes.create({ size: "small" });
const r = await sb.exec("uname -a", { cwd: "/tmp", timeout: 60 });
await sb.files.write("app.py", "print('hi')");
for await (const e of sb.execStream("make test")) if (e.type === "stdout") process.stdout.write(e.data);
// without `await using`: await froog.sandboxes.use({ size: "small" }, async (sb) => { ... })

// Go
c := froog.New("")                                // FROOG_API_KEY from the environment
sb, err := c.Sandboxes.Create(ctx, froog.CreateSandbox{Size: froog.Ptr(froog.SizeSmall)})
if err != nil { return err }
defer sb.Delete(context.Background())
r, err := sb.Exec(ctx, "uname -a", &froog.ExecOptions{Cwd: "/tmp", Timeout: time.Minute})

The sandbox object (Sandbox in Python, SandboxHandle in TypeScript and Go) has:

  • exec(cmd, cwd=, env=, stdin=, timeout=) and exec_stream(...). stdin takes text or bytes.
  • files.read, read_text, write, list, info, exists, remove, mkdir, move and watch (changes under a path as they happen: for e in sb.files.watch("src"): print(e["event"], e["path"])).
  • start and stop, which wait until they're done. stop follows the stop's id.
  • fork(name), which returns the new sandbox once it's running, and delete.
  • snapshot(label), host(port) (returns the URL), metrics and refresh.
  • wait_for_port(port, timeout), which calls POST /wait until something listens on the port (up to 600 seconds) and raises a 408 timeout error if nothing does.

Get a sandbox that already exists with froog.sandboxes.get(name), or list them with froog.sandboxes.list(label="team=a"). Pass a list for several labels, all of which must match: label=["team=a", "tier"] (TypeScript {label: ["team=a", "tier"]}, Go &froog.ListSandboxesParams{Label: []string{"team=a", "tier"}}). A sandbox you got this way is never deleted at the end of a block. In Go, c.Sandbox(name) makes a handle without a request.

Background commands run as jobs, so they survive the connection. spawn returns a handle:

server = sb.spawn("npm run dev", cwd="app", env={"PORT": "3000"})
sb.wait_for_port(3000)
print(sb.host(3000))
print(server.logs())                               # its output so far, stdout and stderr together
server.kill()

build = sb.spawn("make build")
for chunk in build.follow(): print(chunk, end="")  # output as it arrives, until it ends
print(build.wait())                                # the exit code
p = sb.process("job_…")                            # reconnect later, from another program too
sb.processes()                                     # every background command, newest first

repl = sb.spawn("python3 -i", stdin_open=True)    # TypeScript {stdinOpen: true}, Go &ExecOptions{StdinOpen: true}
repl.write("print(6 * 7)\n")                      # text or bytes
repl.close_stdin()                                 # end of file (TypeScript closeStdin, Go CloseStdin)

Python also has an asyncio client with the same methods as coroutines. It stays standard-library only by running each call on a thread pool:

from froogdev import AsyncFroog
froog = AsyncFroog()
async with await froog.sandboxes.create() as sb:
    results = await asyncio.gather(*(sb.exec(f"pytest -k {k}") for k in keys))
    async for e in sb.exec_stream("make"): ...
    await (await sb.spawn("make build")).wait()
print(await froog.list_sandboxes())                # every flat method too

Under the objects, each client has one method per operation, named after its operationId: createSandboxes, exec, readCommandLogs, hostPort and so on. Python uses snake_case, and Go capitalizes the first letter. There are also waitUntil and execStream. These flat methods take path parameters first, then the JSON body, then query parameters. createSandboxes and forkSandbox take an optional idempotency key last. In Go, they take required query parameters as arguments, and optional ones and the idempotency key in an …Params struct.

Errors raise FroogError (Go: return *froog.Error). It carries the HTTP status, the API's error code, the message and the request id: requestId in TypeScript, request_id in Python, RequestID in Go. The TypeScript and Go files include types for every request and response.

# the same calls without objects
froog.create_sandboxes({"name": "box"}, idempotency_key="job-42")
froog.wait_until("box", "running")
print(froog.exec("box", {"command": "uname -a"})["stdout"])