# Python and TypeScript

> The official clients: install, connect, write, search, count, SQL, errors and retries, and every API operation by its operationId.

Two clients, both generated from the [OpenAPI document](https://api.skimmerdb.com/openapi.json)
with a small hand-written layer on top. Both are MIT-licensed and open source:
[skimmerdb-python](https://github.com/SkimmerDB/skimmerdb-python) and
[skimmerdb-js](https://github.com/SkimmerDB/skimmerdb-js).

| | Python | TypeScript |
|---|---|---|
| Install | `pip install skimmerdb` | `npm install skimmerdb` |
| Needs | Python 3.11+ | Node 18+ (or any runtime with `fetch`) |
| Version | 0.1.0 | 0.1.0 |

## Connect

Pass a key, or set `SKIMMERDB_KEY` in the environment. `SKIMMERDB_URL` (or `base_url` /
`baseUrl`) points the client at another server, such as a local one.

```python title="Python"
from skimmerdb import SkimmerDB

db = SkimmerDB("tb_...")          # or SkimmerDB() with SKIMMERDB_KEY set
# new account: db = SkimmerDB.signup("ada@example.com", "correct horse"); db.key is its key
# existing account: db = SkimmerDB.login(email, password) makes a new write key
```

```ts title="TypeScript"
import { SkimmerDB } from "skimmerdb";

const db = new SkimmerDB("tb_...");  // or new SkimmerDB() with SKIMMERDB_KEY set
// new account: const db = await SkimmerDB.signup("ada@example.com", "correct horse"); db.key
// existing account: await SkimmerDB.login(email, password) makes a new write key
```

## Types

Both clients are fully typed, generated from the OpenAPI document: every request and every
answer has a type, so your editor completes field names and your type checker catches typos.

- **TypeScript**: a type for every request and answer (`SearchResult`, `Hit`, `Document`,
  `WriteResult`, `StoredDocument`, `SqlResult`, `CountResult`, ...), exported from `"skimmerdb"`.
- **Python**: attrs classes in `skimmerdb._api.models` (`SearchResult`, `Hit`, `WriteResult`,
  ...), and the package ships `py.typed`, so mypy and pyright check your calls.

```ts title="TypeScript"
import { SkimmerDB, type SearchResult, type Hit, type WriteResult } from "skimmerdb";

const db = new SkimmerDB();
const w: WriteResult = await db.namespace("support").write([{ id: "t-1", text: "Refund for ACME-1042" }]);
const r: SearchResult = await db.namespace("support").search("refund");
const first: Hit | undefined = r.hits[0];
console.log(w.written, first?.id, first?.line, r.temperature);
```

```python title="Python"
from skimmerdb import SkimmerDB
from skimmerdb._api.models import Hit, SearchResult, WriteResult

db = SkimmerDB()
w: WriteResult = db.namespace("support").write([{"id": "t-1", "text": "Refund for ACME-1042"}])
r: SearchResult = db.namespace("support").search("refund")
first: Hit = r.hits[0]
print(w.written, first.id, first.line, r.temperature)
```

### Typed metadata

Give a namespace your metadata's type, and `write()` takes it while `search()` and `get()`
return `meta` typed as it:

```ts title="TypeScript"
type Ticket = { status: "open" | "closed"; priority: number; ts: number };

const tickets = db.namespace<Ticket>("support");
await tickets.write([{ id: "t-1", text: "Refund for ACME-1042", meta: { status: "open", priority: 3, ts: 1760572800 } }]);
const { hits } = await tickets.search("refund", { filter: { status: "open" } });
hits[0]?.meta.priority; // number
```

In Python every answer is a typed model (`SearchResult`, `Hit`, `StoredDocument`, ...) and
`meta` is a plain `dict`. To check your metadata's shape with mypy or pyright, describe it as a
`TypedDict` and cast where you read it:

```python title="Python"
from typing import Literal, TypedDict, cast

class Ticket(TypedDict, total=False):
    status: Literal["open", "closed"]
    priority: int
    ts: int

for hit in ns.search("refund", filter={"status": "open"}).hits:
    meta = cast(Ticket, hit.meta.to_dict())
    meta["priority"]  # int
```

## Namespaces

`db.namespace(name)` sends nothing by itself: the first write makes the namespace if it doesn't
exist. Create one explicitly to fix its vector settings.

```python title="Python"
ns = db.namespace("support")
notes = db.create_namespace("notes", dims=768, metric="cosine", exist_ok=True)
db.namespaces(limit=100)     # .namespaces, .next
ns.info()                    # its counters and stored bytes
ns.drop()                    # deletes it and every document, for good
```

```ts title="TypeScript"
const ns = db.namespace("support");
const notes = await db.createNamespace("notes", { dims: 768, metric: "cosine", existOk: true });
await db.namespaces({ limit: 100 }); // .namespaces, .next
await ns.info();
await ns.drop();
```

## Writing

```python title="Python"
ns.write([{"id": "t-1", "text": "Refund requested for order ACME-1042", "meta": {"status": "open"}}],
         deletes=["t-0"], wait=True)                   # also ns.upsert(...)
ns.update([{"id": "t-1", "meta": {"status": "closed"}}])   # meta merged; None removes a key
ns.delete(["t-1"])
```

```ts title="TypeScript"
await ns.write([{ id: "t-1", text: "Refund requested for order ACME-1042", meta: { status: "open" } }],
  { deletes: ["t-0"], wait: true });
await ns.update([{ id: "t-1", meta: { status: "closed" } }]); // meta merged; null removes a key
await ns.delete(["t-1"]);
```

Each call writes up to 10,000 documents. All three answer a `WriteResult`
(`written`, `deleted`, `version`, `stored_bytes`, `created`).

## Reading

```python title="Python"
r = ns.search("refund", filter={"status": "open"}, limit=50)
for hit in r.hits:
    print(hit.id, hit.line)
more = ns.search("refund", filter={"status": "open"}, cursor=r.next)

ns.search("E[0-9]{3}", regex=True)
ns.search(vector=embedding, limit=10, filter={"team": "api"})   # nprobe=, rerank=, full= too
ns.count("refund")                         # an int; ns.count_result(...) for the whole answer
ns.get("t-1")                              # one document whole
ns.sql("SELECT status, COUNT(*) FROM docs GROUP BY status")   # .columns, .types, .rows
ns.warm(["refund", "ERR-404"])
```

```ts title="TypeScript"
const r = await ns.search("refund", { filter: { status: "open" }, limit: 50 });
r.hits.forEach((h) => console.log(h.id, h.line));
const more = await ns.search("refund", { filter: { status: "open" }, cursor: r.next });

await ns.search("E[0-9]{3}", { regex: true });
await ns.search("", { vector: embedding, limit: 10, filter: { team: "api" } }); // nprobe, rerank, full too
await ns.count("refund");                  // a number; ns.countResult(...) for the whole answer
await ns.get("t-1");
await ns.sql("SELECT status, COUNT(*) FROM docs GROUP BY status");
await ns.warm(["refund", "ERR-404"]);
```

## Keys, account and usage

```python title="Python"
agent_key = db.create_key("support agent", scope="read", namespaces=["support"])   # needs an admin key
db.me()
db.usage()
```

```ts title="TypeScript"
const agentKey = await db.createKey("support agent", { scope: "read", namespaces: ["support"] });
await db.me();
await db.usage();
```

`create_key` / `createKey` also take caps: `rate_per_s`, `cap_queries`, `cap_writes`.

## Embeddings

Let SkimmerDB embed documents' text with your own provider key, or hosted
([Embeddings](/guides/embeddings)), then search by meaning:

```python title="Python"
db.set_embedding_key("openai", "sk-proj-...")          # once; needs an admin key
kb = db.create_namespace("kb", embed={"provider": "openai", "dims": 512})
kb.write([{"id": "refunds", "text": "To get your money back, open the order..."}], wait=True)
kb.search(semantic="how do I get reimbursed", limit=5)
kb.set_embedding("voyage", "voyage-3.5")                # or None to stop
```

```ts title="TypeScript"
await db.setEmbeddingKey("openai", "sk-proj-...");     // once; needs an admin key
const kb = await db.createNamespace("kb", { embed: { provider: "openai", dims: 512 } });
await kb.write([{ id: "refunds", text: "To get your money back, open the order..." }], { wait: true });
await kb.search("", { semantic: "how do I get reimbursed", limit: 5 });
await kb.setEmbedding({ provider: "voyage", model: "voyage-3.5" }); // or null to stop
```

## Errors and retries

An error answer raises `SkimmerDBError` with `status` (HTTP), `message`, `code` (a word to act
on: `not_found`, `invalid`, `rate_limited`, `write_busy`, ...), `fields` (what's wrong with each
invalid field) and `retry_after` / `retryAfter` (seconds, on 429).

Both clients retry 429 and 503 answers by themselves, after `Retry-After` or backing off: 3
retries by default (in TypeScript, `new SkimmerDB(key, { retries })`). Writes are by id, so a
retried write is safe.

```python title="Python"
from skimmerdb import SkimmerDBError

try:
    ns.get("missing")
except SkimmerDBError as e:
    if e.code == "not_found":
        ...
```

```ts title="TypeScript"
import { SkimmerDBError } from "skimmerdb";

try {
  await ns.get("missing");
} catch (e) {
  if (e instanceof SkimmerDBError && e.code === "not_found") { /* ... */ }
}
```

The error codes are listed in [Errors](/reference/errors).

## Every operation

The hand-written layer covers the common calls. Every operation in the
[API reference](/api) is also generated, named by its operationId, with typed models:

```python title="Python"
from skimmerdb._api.api.search import search
from skimmerdb._api.models import SearchRequest

search.sync(name="support", client=db.api, body=SearchRequest(q="refund"))
```

```ts title="TypeScript"
import { search } from "skimmerdb/api";

const { data } = await search({ client: db.client, path: { name: "support" }, body: { q: "refund" } });
```

`db.api` (Python) and `db.client` (TypeScript) are the authenticated clients to pass them.
