Connect
SDKs
TypeScript, Python, Go and Rust clients that pass one shared test suite. Paths instead of ids, streaming uploads and ranged downloads, safe writes, edits, sidecars, search, changes, webhooks and actions.
| Language | Package | Folder | Runs on |
|---|---|---|---|
| TypeScript / JavaScript | @sidespace/sdk | sdks/typescript | Node 18+, Deno, Bun, browsers (anything with fetch) |
| Python | sidespace | sdks/python | Python 3.9+, standard library only |
| Go | github.com/blendduck/sidespace/sdks/go | sdks/go | Go 1.22+, standard library only |
| Rust | sidespace | sdks/rust | Async, on reqwest (rustls) and tokio |
All four are Apache-2.0 and cover the same ground for files. They are not on npm, PyPI, the Go module proxy or crates.io yet: use them from a checkout of the repository, or copy the folder into your project.
npm install ./sdks/typescriptpip install ./sdks/python# in your module, point the import at the checkout
go mod edit -replace github.com/blendduck/sidespace/sdks/go=../sidespace/sdks/go
go get github.com/blendduck/sidespace/sdks/go# Cargo.toml
[dependencies]
sidespace = { path = "../sidespace/sdks/rust" }Set up
import { SideSpace } from "@sidespace/sdk";
const side = new SideSpace({ baseUrl: "https://side.example.com", token: process.env.SIDESPACE_TOKEN! });from sidespace import SideSpace
side = SideSpace("https://side.example.com", token="ssp_…") # or SideSpace() with SIDESPACE_URL and SIDESPACE_TOKEN setimport sidespace "github.com/blendduck/sidespace/sdks/go"
side := sidespace.New("https://side.example.com", os.Getenv("SIDESPACE_TOKEN"))
hits, err := side.Search(ctx, "harbor night", nil)use sidespace::SideSpace;
let side = SideSpace::new("https://side.example.com", &std::env::var("SIDESPACE_TOKEN")?);
let hits = side.search("harbor night", &Default::default()).await?;Spaces can be given by name or id, and files by path. Errors carry the server's status, code and message: SideSpaceError in TypeScript and Python, and in Go and Rust an error with IsPreconditionFailed / is_precondition_failed() and the current version for a safe write that lost. Each folder's README has the full method list.
What's covered
| Area | TypeScript | Python |
|---|---|---|
| Spaces and folders | spaces(), space(), node(), list(), mkdir() | spaces(), space(), node(), list(), mkdir() |
| Upload and download | upload(), download(), open() with range | upload() (streams file objects), download(), open() with range |
| Versions | versions(), restoreVersion() | versions(), restore_version() |
| Part of a file | edit(), appendText() | edit(), append_text() |
| The past | asOf on node(), list(), open(), download(); rewind() | as_of= on node(), list(), open(); rewind() |
| Sidecars | getSidecar(), putSidecar() | get_sidecar(), put_sidecar() |
| Search | search(q, { space, type, mode }) | search(q, space=, type=, mode=) |
| Changes | journal(), changes() (async iterator) | journal(), changes() (generator) |
| Webhooks | createWebhook(), verifyWebhookSignature() from @sidespace/sdk/webhooks | create_webhook(), verify_webhook_signature() |
| Actions and pipelines | actions(), run(), startRun(), waitRun(), runs(), createPipeline() | actions(), run(), start_run(), wait_run(), runs(), create_pipeline() |
| Comments | comments(), addComment(), editComment(), resolveComment() | comments(), add_comment(), edit_comment(), resolve_comment() |
| Forks | fork(), forks(), forkChanges(), applyFork() | fork(), forks(), fork_changes(), apply_fork() |
| Plugins | installPlugin(), PluginRun | install_plugin(), PluginRun |
Go and Rust cover the same file operations (spaces, paths, streaming upload and ranged download, safe writes, byte-range edits, versions, sidecars, search, comments, changes, actions and webhooks), with Go and Rust names (UploadText, upload_text).
Anything else in the API reference is one request() away (Do in Go):
const film = await side.space("Film A");
const presence = await side.request("GET", `/v1/spaces/${film.id}/presence`);film = side.space("Film A")
presence = side.request("GET", f"/v1/spaces/{film['id']}/presence")One test suite for all four
sdks/conformance/cases.json is one list of 34 steps (save, read ranges, safe-write conflicts, edits, restore, folders, sidecars, comments, search and errors) that every SDK runs against a live server, each through its own runner. check.py compares their answers, so the four SDKs can't drift apart. All four pass. To check one yourself against a dev server:
SIDESPACE_SERVER=… SIDE_TOKEN=… SPACE=… python3 sdks/conformance/runner.py | python3 sdks/conformance/check.py --sdk pythonA new SDK, in any language, is done when it passes every step.
A complete example
Score every clip in /dailies that hasn't been scored yet with your own model, reading only the first 8 MB of each, and keep the result in a sidecar:
import { SideSpace, SideSpaceError } from "@sidespace/sdk";
const side = new SideSpace({ baseUrl: process.env.SIDESPACE_URL!, token: process.env.SIDESPACE_TOKEN! });
for (const file of await side.list("Film A", "/dailies")) {
if (!file.mime?.startsWith("video/")) continue;
const path = `/dailies/${file.name}`;
const done = await side.getSidecar("Film A", path, "x-mymodel").catch((e) => {
if (e instanceof SideSpaceError && e.status === 404) return null;
throw e;
});
if (done) continue;
const head = await side.open("Film A", path, { range: [0, 8 * 1024 * 1024 - 1] });
const score = await myModel(await head.arrayBuffer());
await side.putSidecar("Film A", path, "x-mymodel", { score, model: "mymodel-2" });
}from sidespace import SideSpace, SideSpaceError
side = SideSpace()
for file in side.list("Film A", "/dailies"):
if not (file.get("mime") or "").startswith("video/"):
continue
path = f"/dailies/{file['name']}"
try:
side.get_sidecar("Film A", path, "x-mymodel")
continue # already scored
except SideSpaceError as e:
if e.status != 404:
raise
with side.open("Film A", path, range=(0, 8 * 1024 * 1024 - 1)) as r:
score = my_model(r.read())
side.put_sidecar("Film A", path, "x-mymodel", {"score": score, "model": "mymodel-2"})To keep up with new clips as they land, run the same code from a webhook, or package it as a plugin and add a pipeline: both keep their place and retry for you.