# 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.

```sh tab=TypeScript
npm install ./sdks/typescript
```

```sh tab=Python
pip install ./sdks/python
```

```sh tab=Go
# 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
```

```toml tab=Rust
# Cargo.toml
[dependencies]
sidespace = { path = "../sidespace/sdks/rust" }
```

## Set up

```ts tab=TypeScript
import { SideSpace } from "@sidespace/sdk";

const side = new SideSpace({ baseUrl: "https://side.example.com", token: process.env.SIDESPACE_TOKEN! });
```

```python tab=Python
from sidespace import SideSpace

side = SideSpace("https://side.example.com", token="ssp_…")   # or SideSpace() with SIDESPACE_URL and SIDESPACE_TOKEN set
```

```go tab=Go
import 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)
```

```rust tab=Rust
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](/docs/api) is one `request()` away (`Do` in Go):

```ts tab=TypeScript
const film = await side.space("Film A");
const presence = await side.request("GET", `/v1/spaces/${film.id}/presence`);
```

```python tab=Python
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:

```sh
SIDESPACE_SERVER=… SIDE_TOKEN=… SPACE=… python3 sdks/conformance/runner.py | python3 sdks/conformance/check.py --sdk python
```

A 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:

```ts tab=TypeScript
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" });
}
```

```python tab=Python
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](/docs/changes#webhooks), or package it as a [plugin](/docs/plugins) and add a pipeline: both keep their place and retry for you.
