Metadata-Version: 2.4
Name: instanode
Version: 0.2.0
Summary: Official Python SDK for instanode.dev — zero-friction developer infrastructure for AI agents
License: MIT
Project-URL: Homepage, https://instanode.dev
Project-URL: Documentation, https://instanode.dev/docs
Project-URL: Repository, https://github.com/InstaNode-dev/sdk-python
Project-URL: Bug Tracker, https://github.com/InstaNode-dev/sdk-python/issues
Keywords: postgres,redis,mongodb,nats,developer-tools,ai-agents
Classifier: Development Status :: 3 - Alpha
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: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# instanode

Official Python SDK for [instanode.dev](https://instanode.dev) — zero-friction developer
infrastructure. Provision real databases, caches, queues, and heartbeat monitors with
a single function call. No account required. No Docker. No setup.

```
pip install instanode
```

---

## Quickstart

### Heartbeat monitor (cron job health check)

```python
import instanode

client = instanode.Client()

# Provision a monitor — no account needed
mon = client.provision_monitor(name="nightly-backup")
print(mon.url)  # https://instanode.dev/ping/<token>

# Wrap any callable — sends /start, then success or /fail automatically
def run_backup():
    ...  # your job logic here

client.heartbeat_wrap(mon.token, run_backup)
```

### Postgres database

```python
import instanode

client = instanode.Client()
db = client.provision_database(name="my-app-db")

# Use the connection string with psycopg2, SQLAlchemy, Django, asyncpg, etc.
print(db.connection_url)
# postgres://user:pass@host:5432/dbname

# Anonymous resources expire in 24 h. Check the note field:
if db.note:
    print(db.note)  # "Claim your resources at https://instanode.dev/start?t=..."
```

### Full provision (DB + cache + MongoDB + monitor)

```python
import instanode

client = instanode.Client()

db    = client.provision_database(name="app-db")
cache = client.provision_cache(name="app-cache")
mongo = client.provision_mongodb(name="app-nosql")
mon   = client.provision_monitor(name="app-health")

print(db.connection_url)     # postgres://...
print(cache.connection_url)  # redis://...
print(mongo.connection_url)  # mongodb://...
print(mon.url)               # https://instanode.dev/ping/<token>
```

---

## Authentication

Without an API key, the SDK operates in **anonymous mode**: resources are real but
expire after 24 hours and have lower rate limits (100 pings/day, 10 MB Postgres, etc.).

To make resources permanent, call `client.claim()` or visit the upgrade URL that
appears in `result.note`.

```python
# Set via environment variable (recommended)
export INSTANODE_API_KEY=sk_...

# Or pass to the constructor
client = instanode.Client(api_key="sk_...")
```

---

## Method Reference

| Method | Endpoint | Auth required |
|---|---|---|
| `provision_monitor(name)` | `POST /ping/new` | No |
| `heartbeat(token)` | `POST /ping/{token}` | No |
| `heartbeat_start(token)` | `POST /ping/{token}/start` | No |
| `heartbeat_fail(token)` | `POST /ping/{token}/fail` | No |
| `heartbeat_wrap(token, fn)` | start + success/fail | No |
| `provision_database(name)` | `POST /db/new` | No |
| `provision_cache(name)` | `POST /cache/new` | No |
| `provision_mongodb(name)` | `POST /nosql/new` | No |
| `provision_queue(name)` | `POST /queue/new` | No |
| `claim(jwt, email, team_name)` | `POST /claim` | No |
| `list_resources()` | `GET /api/v1/resources` | Yes |
| `get_resource(token)` | `GET /api/v1/resources/{token}` | Yes |
| `delete_resource(token)` | `DELETE /api/v1/resources/{token}` | Yes |
| `rotate_credentials(token)` | `POST /api/v1/resources/{token}/rotate-credentials` | Yes |

---

## Tier Limits

| Tier | Postgres | Redis | MongoDB | Pings/day |
|---|---|---|---|---|
| anonymous | 10 MB / 2 conn / 24 h | 5 MB / 24 h | 5 MB / 2 conn / 24 h | 100 |
| hobby (claimed) | 500 MB / 5 conn | 25 MB | 100 MB / 5 conn | 1 000 |
| pro ($12/mo) | 5 120 MB / 20 conn | 256 MB | 2 048 MB / 20 conn | 50 000 |
| team ($49/mo) | unlimited | unlimited | unlimited | unlimited |

---

## Error handling

```python
import instanode

client = instanode.Client()

try:
    db = client.provision_database()
except instanode.InstanodeError as exc:
    print(exc.status_code)  # 503
    print(exc.code)         # "provisioning_failed"
    print(exc.message)      # "provisioner unavailable"

    if exc.is_rate_limited():      # 429
        print("slow down")
    elif exc.is_service_unavailable():  # 503
        print("try again later")
    elif exc.is_not_found():       # 404
        print("wrong token")
    elif exc.is_conflict():        # 409  — JWT already claimed
        print("already claimed")
```

---

## Context manager (auto-cleanup)

The client supports use as a context manager. No persistent connections are held,
so this is mainly for idiomatic resource scoping:

```python
import instanode

with instanode.Client() as client:
    db = client.provision_database()
    # use db.connection_url ...
# client goes out of scope here
```

---

## Async support

```python
import asyncio
import instanode.aio

async def main():
    async with instanode.aio.AsyncClient() as client:
        # Provision concurrently
        db, cache = await asyncio.gather(
            client.provision_database(name="app-db"),
            client.provision_cache(name="app-cache"),
        )
        print(db.connection_url)
        print(cache.connection_url)

        # heartbeat_wrap works with async functions too
        async def run_job():
            await asyncio.sleep(0.1)

        mon = await client.provision_monitor(name="async-job")
        await client.heartbeat_wrap(mon.token, run_job)

asyncio.run(main())
```

---

## Claiming anonymous resources

When instanode.dev surfaces the upgrade URL in `result.note`, extract the JWT from
the `?t=` query parameter and call `claim()`:

```python
import instanode
from urllib.parse import urlparse, parse_qs

client = instanode.Client()
db = client.provision_database()

if db.note and "instanode.dev/start" in (db.note or ""):
    # parse the JWT from the URL in the note
    # e.g. note = "Claim at https://instanode.dev/start?t=eyJ..."
    upgrade_url = db.note.split()[-1]
    jwt = parse_qs(urlparse(upgrade_url).query)["t"][0]

    result = client.claim(
        jwt=jwt,
        email="dev@example.com",
        team_name="My Team",   # optional
    )
    print(f"Claimed {result.claimed} resources → team {result.team_id}")
```

---

## Zero dependencies

The SDK uses only the Python standard library (`urllib.request`). No `requests`,
no `httpx`, no `aiohttp`. Works in any environment including AWS Lambda, Google
Cloud Functions, and minimal Docker images.

---

## Links

- Homepage: https://instanode.dev
- Documentation: https://instanode.dev/docs
- GitHub: https://github.com/InstaNode-dev/sdk-python
- PyPI: https://pypi.org/project/instanode
