Metadata-Version: 2.4
Name: nodedata
Version: 0.1.2
Summary: Official Python SDK for the Node Data API — robotics and physical-AI models, datasets, payouts, webhooks and inference.
Project-URL: Homepage, https://nodedata.ai
Project-URL: Documentation, https://nodedata.ai/docs/sdks
Project-URL: Source, https://github.com/Node-Data/forge-robotics/tree/main/packages/sdk-python
Author-email: "Node, Inc." <casper@nodedata.ai>
License: MIT
License-File: LICENSE
Keywords: datasets,machine-learning,nodedata,physical-ai,robotics
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# nodedata — Python SDK

Official Python client for the [Node Data](https://www.nodedata.ai) API: search
and download robotics/physical-AI models and datasets, publish your own, read
your payouts, subscribe to webhooks, and run Node Data's paid inference.

**Zero runtime dependencies.** Standard library only, so it installs onto a
Jetson next to whatever pinned torch/numpy stack is already there.

```bash
pip install nodedata
pip install -e ".[dev]"       # from this directory, for development
```

[![PyPI](https://img.shields.io/pypi/v/nodedata)](https://pypi.org/project/nodedata/)

Requires Python 3.9+.

## Quick start

Create a key at [/dashboard/api-keys](https://www.nodedata.ai/dashboard/api-keys)
(`nd_test_…` or `nd_live_…`) and export it:

```bash
export NODE_DATA_API_KEY=nd_live_...
```

```python
from nodedata import NodeData

nd = NodeData()                        # reads NODE_DATA_API_KEY

me = nd.me()
print(me.name, me.key.mode, me.key.scopes)

# Search the marketplace
for listing in nd.models.iter_all(q="grasp", type="dataset", max_items=50):
    print(listing.slug, listing.price.dollars, listing.url)

# Download something you own or that's free
path = nd.models.download("acme-panda-grasp-100k", "./cache")

# Publish a build artifact — uploads the file, then creates the listing
listing = nd.models.publish(
    "dist/grasp-policy-v2.onnx",
    title="Grasp policy v2",
    description="Trained on 100k teleop episodes.",
    type="policy",
    price_cents=4900,                  # 0 for free; paid minimum is 100
    frameworks=["pytorch"],
)
print(listing.url)
```

## What's covered

| Area | Calls |
| --- | --- |
| Account | `nd.me()` |
| Listings | `nd.models.list()` · `iter_all()` · `retrieve()` · `create()` · `update()` · `unpublish()` |
| Files | `nd.models.download()` · `download_url()` · `nd.uploads.upload_file()` · `create()` · `confirm()` · `rules()` |
| Publish | `nd.models.publish()` (upload + create in one call) |
| Payouts | `nd.payouts.retrieve()` |
| Usage | `nd.usage.retrieve(days=30)` |
| Webhooks | `nd.webhooks.list()` · `create()` · `retrieve()` · `update()` · `delete()` · `deliveries()` · `event_types()` |
| Inference | `nd.inference.models()` · `chat()` · `stream()` · `stream_text()` · `ask()` |
| Anything newer | `nd.request("GET", "/some/new/endpoint")` |

`nd.datasets` is an alias of `nd.models` — one endpoint serves both.

Every returned object keeps the raw response in `.raw`, so a field the API adds
after this release is still readable without upgrading.

## Pagination

`list()` returns one `Page`; `iter_all()` walks every page lazily.

```python
page = nd.models.list(limit=100)
print(len(page), page.has_more, page.next_cursor)

for listing in nd.models.iter_all(type="dataset"):   # follows cursors
    ...
```

## Inference

Requires a **premium** key with the `inference:run` scope. The endpoint is
OpenAI-compatible, so `openai` pointed at `https://www.nodedata.ai/api/v1` works
too — these helpers just avoid the dependency.

```python
for model in nd.inference.models():
    print(model.id, model.context_window, model.pricing.input_per_1m)

print(nd.inference.ask("Summarise this dataset card.", model="node-reason-1"))

for text in nd.inference.stream_text(
    model="node-reason-1",
    messages=[{"role": "user", "content": "Explain grasp policies"}],
):
    print(text, end="", flush=True)
```

The final streamed chunk carries `usage` and no text — that's the frame to log
spend against.

## Webhooks

Verify with the **raw** request body. Re-serialising parsed JSON changes bytes,
and the signature won't match.

```python
from nodedata import verify_webhook, WebhookVerificationError

endpoint = nd.webhooks.create(
    "https://example.com/hooks/nodedata",
    events=["listing.purchased", "payout.paid"],
)
print(endpoint.secret)   # returned on create only — store it now

# in your handler
try:
    event = verify_webhook(request.body, request.headers["nd-signature"], SECRET)
except WebhookVerificationError:
    return 400
```

`verify_webhook` enforces HMAC-SHA256 with a constant-time compare and a 300s
replay window, matching the server.

## Errors

Everything raises a subclass of `NodeDataError`:

| Exception | Status | Typical cause |
| --- | --- | --- |
| `InvalidRequestError` | 400 | validation failed |
| `AuthenticationError` | 401 | missing, revoked or expired key |
| `PaymentRequiredError` | 402 | `premium_required`, `purchase_required`, unpaid key |
| `PermissionDeniedError` | 403 | key lacks the scope |
| `NotFoundError` | 404 | no such object — or not yours |
| `RateLimitError` | 429 | carries `retry_after` |
| `MaintenanceError` | 503 | platform deliberately closed; carries `retry_after` |
| `ServerError` | 5xx | API failure |
| `APIConnectionError` / `APITimeoutError` | — | never reached the API |

Branch on `exc.code` (`purchase_required`, `scope_required`, `price_too_low`, …)
rather than the message text.

```python
from nodedata import PaymentRequiredError

try:
    nd.models.download("some-paid-asset", "./cache")
except PaymentRequiredError as exc:
    if exc.code == "purchase_required":
        print(f"costs ${exc.body['price_cents'] / 100:.2f}")
```

## Retries

Rate limits and transient failures are retried twice by default with jittered
backoff, honouring `Retry-After`. `POST` is only retried on a 429 — the server
rejected it before doing any work — so a publish can never double-apply.

```python
nd = NodeData(timeout=30, max_retries=5)
```

## CLI

Installing the package also installs a `nodedata` command:

```bash
nodedata whoami
nodedata search grasp --type dataset
nodedata download acme-panda-grasp-100k -o ./cache
nodedata publish dist/policy.onnx --title "Grasp policy" --description "..." --type policy
nodedata payouts
nodedata usage --days 7
nodedata ask "Explain grasp policies" --model node-reason-1
```

## Configuration

| Env var | Purpose |
| --- | --- |
| `NODE_DATA_API_KEY` | API key used when none is passed |
| `NODE_DATA_BASE_URL` | Override the API base URL (preview deploys, self-host) |

## Certificates

Because this SDK uses `urllib` rather than bundling `certifi`, TLS trust comes
from the system store. Some Python builds — notably the macOS python.org
installers — ship an empty one, which fails every HTTPS request with
`CERTIFICATE_VERIFY_FAILED`. Three fixes, in order of ease:

```bash
pip install certifi                              # used automatically if present
open "/Applications/Python 3.x/Install Certificates.command"
```

```python
import ssl
nd = NodeData(ssl_context=ssl.create_default_context(cafile="/path/to/ca.pem"))
```

The SDK detects this specific failure and says so, rather than reporting it as
an API outage.

## Tests

```bash
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest                      # hermetic; runs a local HTTP server
NODE_DATA_LIVE_KEY=nd_live_... .venv/bin/python -m pytest tests/test_live.py -v
```

The suite talks to a real socket rather than a patched transport, so retries,
SSE framing and streamed downloads are exercised as they run in production.
The live suite is read-only.

## Versioning

Minor releases may add response fields. Pin in CI and bump deliberately.

MIT © Node, Inc.
