Metadata-Version: 2.4
Name: fluidghost
Version: 1.0.0
Summary: Official Python client for the FluidGhost API — submit a photo or video, get back distinct uniquified variants with device-authentic metadata.
Author: FluidGhost
License: MIT
Project-URL: Homepage, https://ghost.fluidvip.com
Project-URL: Documentation, https://ghost.fluidvip.com/app/docs/
Keywords: fluidghost,metadata,exif,image,video,uniquify,sdk
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Intended Audience :: Developers
Classifier: Topic :: Multimedia :: Graphics
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25
Dynamic: license-file

# fluidghost

Official Python client for the [FluidGhost API](https://ghost.fluidvip.com).

FluidGhost turns one photo or video into a set of **distinct variants** — each a fresh,
standalone capture with its own metadata, its own pixels, and a device-authentic filename.
This SDK wraps the HTTP/multipart/auth layer so you can **submit → poll → download** in a
few calls.

- Full API docs: <https://ghost.fluidvip.com/app/docs/>
- Python **3.8+** (depends on `requests`).

## Install

```bash
pip install fluidghost
```

## Authentication

Mint an API key in the dashboard (**Settings → API keys** at <https://ghost.fluidvip.com>).
Keep it server-side — never ship it in client code.

- `fg_sk_...` — **secret / data plane**: submit jobs, read results, manage presets.
- `fg_mgmt_...` — **management**: provisioning only (mint sub-keys for your own users).

## Quickstart — submit, wait, download

`run()` submits a job and waits for it to finish. API-key jobs deliver results as
**one-time links**: `download_variant()` streams the bytes and the object is deleted on a
successful download (a second call raises `FluidGhostError` with status `410`).

```python
import os
from fluidghost import FluidGhost

fg = FluidGhost(api_key=os.environ["FLUIDGHOST_KEY"])

job = fg.run(
    image="photo.jpg",
    recipe_id="full-refresh",
    copies=3,
    options={"noise": 35, "ultraSafe": True},
    on_poll=lambda j: print(j["status"], j.get("progress")),
)

for v in job["variants"]:
    if v["status"] != "completed":
        continue
    data = fg.download_variant(job["jobId"], v["index"])
    with open(v.get("fileName") or f"variant-{v['index']}.jpg", "wb") as f:
        f.write(data)
    print(v.get("fileName"), "risk", v.get("detectionRisk"), v.get("riskBand"))
```

You can pass in-memory bytes instead of a path with a `(filename, bytes)` tuple:

```python
fg.create_job(image=("photo.jpg", my_bytes), recipe_id="full-refresh")
```

## Settings

Provide exactly one recipe source (or bind a default preset to your key and omit all three):

| Argument | Meaning |
| --- | --- |
| `recipe_id` | A bundled starter, e.g. `"full-refresh"` (see `get_options()["recipes"]`). |
| `preset_id` | A saved preset, yours or a system one (`"sys:full-refresh"`, `"sys:metadata-only"`). |
| `recipe_config` | A full recipe graph (advanced). |
| `copies` | Number of variants, 1–50. |
| `strength` | Global strength 0–100. |
| `options` | Extra knobs merged onto the recipe, e.g. `{"noise": 35, "ultraSafe": True}`. |

## Other calls

```python
fg.me()                    # {"ownerId": ..., "balance": ..., "tier": ...}
fg.get_options()           # device/city presets, filters, starter recipes, limits
fg.list_jobs()             # your recent jobs
fg.inspect("photo.jpg")    # read EXIF/GPS/MakerNotes, no job

# Presets
fg.list_presets()
fg.create_preset("My look", recipe_config)
fg.update_preset(preset_id, name="Renamed")
fg.delete_preset(preset_id)
```

### Provisioning (management key)

With an `fg_mgmt_...` key you can mint data-plane sub-keys for your own end users:

```python
mgmt = FluidGhost(api_key=os.environ["FLUIDGHOST_MGMT_KEY"])
sub = mgmt.provision_key(identity="user_123", label="Acme user 123")
print(sub["key"])   # shown once — hand it to that user's integration
```

## Errors

Every non-2xx response raises `FluidGhostError`, mirroring the API's `{error, message, details}`:

```python
from fluidghost import FluidGhost, FluidGhostError

try:
    fg.run(image="photo.jpg", recipe_id="full-refresh", copies=3)
except FluidGhostError as e:
    print(e.status, e.code, e.details)
    if e.code == "rate_limited":
        time.sleep(e.retry_after or 5)
    if e.code == "insufficient_balance":
        ...  # top up in the dashboard
```

## Configuration

```python
FluidGhost(
    api_key="fg_sk_live_…",
    base_url="https://api-ghost.fluidvip.com",  # default; override for a proxy
    timeout=60.0,
)
```

## License

MIT
