Metadata-Version: 2.4
Name: sibfly
Version: 0.2.0
Summary: Zero-dependency client for the SibFly ground-motion API (satellite-measured land subsidence/uplift, mm/yr, for any US address)
Author-email: SibFly <contact@sibfly.com>
License: MIT
Project-URL: Homepage, https://sibfly.com
Project-URL: Documentation, https://sibfly.com/llms.txt
Project-URL: API Reference, https://sibfly.com/openapi.json
Keywords: insar,subsidence,ground-motion,geospatial,nasa,opera,sentinel-1
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
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 :: GIS
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# sibfly

Zero-dependency Python client for [SibFly](https://sibfly.com) — satellite-measured
ground motion (sinking/uplift, mm/yr and in/yr) for any US address, from NASA
OPERA Sentinel-1 InSAR. Flat $0.40 per covered report; misses are free.

Pure stdlib (`urllib`), Python >= 3.8, fully typed (`py.typed`), no dependencies.

```bash
pip install sibfly
```

## Quickstart

```python
from sibfly import SibFly, InsufficientCredits

client = SibFly.register("agent@example.com")          # self-onboard: key + free credits
try:
    r = client.motion(address="425 Fremont St, Las Vegas, NV")
    print(r["velocity_vertical_mm_yr"], r["assessment"])
except InsufficientCredits as e:
    print("top up at:", e.top_up_url)                   # self-refill URL from the 402
```

Already have a key? `SibFly(api_key="sf_...")` or set `SIBFLY_API_KEY` in the env.

## Out of credits? The full recovery loop

When a billed call raises `InsufficientCredits`, you can self-refill entirely
by API — no browser, no human:

```python
import time
from sibfly import SibFly, SpendCapReached, InsufficientCredits

client = SibFly()  # SIBFLY_API_KEY in env

def motion_with_refill(**q):
    try:
        return client.motion(**q)
    except SpendCapReached:
        raise            # your own daily cap — topping up won't help; see below
    except InsufficientCredits as e:
        order = client.buy(10)                      # POST /api/v1/buy -> Stripe session
        print("pay here:", order["checkout_url"])   # open/relay this URL
        while client.balance()["credits_usd"] < 0.40:
            time.sleep(15)                          # credits land via webhook
        return client.motion(**q)                   # now it goes through

report = motion_with_refill(address="425 Fremont St, Las Vegas, NV")
```

`e.buy_api` on the exception carries the machine-readable refill recipe from the
402 body (suggested amount, endpoints). `client.buy(amount, method="crypto")`
returns `{invoice_url, txn_id}` for BTC instead of a Stripe `checkout_url`.

### Spend caps

`client.spend_cap(5)` sets a per-key daily cap of $5; once tripped, billed calls
raise `SpendCapReached` (a subclass of `InsufficientCredits`, so old handlers
still catch it — but don't top up: you still have credits, the cap is yours).
Clear it with `client.spend_cap(None)`.

## Billed retries and Idempotency-Key — read this

`motion()`, `batch()`, and `timeseries()` are **billed**. Retrying a billed call
**without** an `Idempotency-Key` charges you **again**. The SDK protects you: it
auto-sets a fresh UUID `Idempotency-Key` header on every billed call, so its own
internal retries (network blips, 5xx) can never double-charge.

But the auto key is **per call** — if *your code* re-invokes `motion(...)` after
a crash, that is a new key and a new charge. To make your own retries free, pass
an explicit key and reuse it:

```python
r = client.motion(address="...", idempotency_key="job-1234-row-7")
# same call again with the same key -> served from cache, cost_usd == 0
```

Replays are cached for 7 days.

## Surface

| Method | Endpoint | Billed |
|---|---|---|
| `SibFly.register(email)` | `POST /api/v1/autonomous/register` — returns a ready client | free |
| `client.motion(address=... / lat=..., lon=..., **gates)` | `GET /api/v1/motion` | $0.40 (misses free) |
| `client.batch(items, async_=True)` | `POST /api/v1/motion/batch` (max 1000; only covered rows billed) | per covered row |
| `client.batch_job(job_id)` / `client.wait_batch(job_id)` | `GET /api/v1/motion/batch/{job_id}` | free to poll |
| `client.timeseries(...)` | `GET /api/v1/timeseries` | yes |
| `client.buy(amount_usd, method="stripe")` | `POST /api/v1/buy` -> payment URL | free (payment link) |
| `client.spend_cap(daily_usd)` | `POST /api/v1/account/spend_cap` (None clears) | free |
| `client.coverage(...)` / `client.coverage_batch(items)` | `GET /api/v1/coverage` / `POST /api/v1/coverage/batch` | free |
| `client.frames()` / `client.frame_last_updated(id)` | `GET /api/v1/frames` / `/frames/{id}/last_updated` | free |
| `client.geocode(address)` | `GET /api/v1/geocode` | free |
| `client.balance()` / `client.me()` / `client.usage()` | `GET /api/v1/balance` / `/me` / `/usage` | free |
| `client.create_key(daily_cap_usd=..)` / `list_keys()` / `revoke_key(k)` | `POST/GET /api/v1/keys`, `DELETE /api/v1/keys/{key}` | free |

Gates (free-miss guards) pass straight through: `motion(address=..., dry_run=1)`,
`max_age_days=90`, `min_confidence=0.8`, `include="timeseries"`, `brief=1`, ...

## Built in

- **Auto-retry** — configurable via `SibFly(api_key, timeout=30, max_retries=3,
  max_backoff=30)`: exponential backoff **with jitter** on 429/5xx/network
  errors, honors `Retry-After`.
- **Idempotency** — a UUID `Idempotency-Key` header is auto-attached to billed
  calls (`motion`, `batch`, `timeseries`), so a retried request is never
  double-charged. Pass `idempotency_key="..."` to control it.
- **Typed errors** — `AuthError` (401), `InsufficientCredits` (402, carries
  `.top_up_url` / `.buy_api` / `.suggested_top_up_usd`), `SpendCapReached`
  (402 `spend_cap_reached`, subclass of `InsufficientCredits`),
  `RateLimitError` (429 after retries, carries `.retry_after`), `NetworkError`
  (timeouts/connection failures, `retryable=True`), `SibFlyError` base
  (`.status`, `.code`, `.request_id`, `.body`).
- **Typing** — full annotations + `py.typed`; `MotionReport`, `Balance`,
  `BuyResult` TypedDicts.

Full API contract: https://sibfly.com/llms.txt · MIT license.
