Metadata-Version: 2.5
Name: streamgine
Version: 1.0.0
Summary: Streamgine Python SDK — Diff by Streamgine webhooks client and future modules.
Project-URL: Homepage, https://streamgine.com
Project-URL: Documentation, https://diff.streamgine.com/docs.md
Project-URL: Repository, https://github.com/scurtutech/diff-events-service
Project-URL: Issues, https://github.com/scurtutech/diff-events-service/issues
Project-URL: Changelog, https://github.com/scurtutech/diff-events-service/releases
Author-email: Streamgine <hello@streamgine.com>
License: MIT
Keywords: agents,compliance,corporate-standing,diff,streamgine,webhooks
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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 :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# streamgine — Python SDK

**Diff by Streamgine** lives under the `diff` module. Install once; add more Streamgine modules (training data, etc.) under the same package later.

This directory is the **customer package** published to PyPI. Repo-only load tests and dev agents live in [`tools/python/`](../../tools/python/README.md) — not shipped to customers.

**Today:** one wheel `streamgine` = Diff (`from streamgine import diff`). **Next products** (e.g. training-data) get their own wheels (`streamgine-diff`, `streamgine-training`, …) under the same `streamgine` import namespace — see [PUBLISHING.md](PUBLISHING.md) and `.cursor/rules/python-client.mdc`. Do not grow a fat single package.

**Product:** [diff.streamgine.com](https://diff.streamgine.com) · **Docs:** [docs.md](https://diff.streamgine.com/docs.md) · **California:** [coverage](https://diff.streamgine.com/california) · **Use cases:** [risk monitoring](https://diff.streamgine.com/use-cases/risk-monitoring), [AI agents](https://diff.streamgine.com/use-cases/ai-agents) · **Repo:** [github.com/scurtutech/diff-events-service](https://github.com/scurtutech/diff-events-service)

```python
from streamgine import diff

# Once at startup — pick ONE download from /account:
diff.configure("diff-streamgine-credentials.json")
# OR: source diff-streamgine-env.sh  then  diff.configure()
client = diff.DiffClient()
```

Install from PyPI:

```bash
python3 -m pip install streamgine
```

Or from this repo:

```bash
cd clients/python
python3 -m venv .venv && source .venv/bin/activate
python -m pip install .
```

Requires Python 3.10+. One dependency: [httpx](https://www.python-httpx.org/) (TLS verification enabled by default).

## Credentials (one setup point)

Call `diff.configure()` **once** at process startup. Every `DiffClient()` and `verify_webhook()` reads from that config.

Download from `/account` or checkout — filenames are fixed:

| Download                           | Then                                                    |
| ---------------------------------- | ------------------------------------------------------- |
| `diff-streamgine-credentials.json` | `diff.configure("diff-streamgine-credentials.json")`    |
| `diff-streamgine-env.sh`           | `source diff-streamgine-env.sh` then `diff.configure()` |

**Option A — credentials JSON:**

```python
diff.configure("diff-streamgine-credentials.json")
```

**Option B — env script:**

```bash
source diff-streamgine-env.sh
```

```python
diff.configure()
```

Do **not** pass API keys or signing secrets to `DiffClient()` or `verify_webhook()` in normal use.

## Zero-setup agent (copy-paste)

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e . fastapi uvicorn
# Customer path: download from /account, then ONE of:
#   source diff-streamgine-env.sh
#   # or keep diff-streamgine-credentials.json next to the process
# Local repo seed (AUTH_ENABLED): npm run seed:dev-customer, then export printed keys
source diff-streamgine-env.sh   # or configure("diff-streamgine-credentials.json") in code
python -m uvicorn examples.agent:app --port 8000
```

Runnable source: [`examples/agent.py`](examples/agent.py). Calls `register_test()` on startup; within ~1s you should see `after={'test': 'success'}` on topic `diff-event.test`.

**Serverless (AWS Lambda):** [`examples/serverless/`](examples/serverless/README.md) — deploy `handler.py`, run `register.py` once, receive test heartbeats in CloudWatch.

**Agent journey (production filters):** [`examples/agent_journey.py`](examples/agent_journey.py).

Docker worker reaching a host agent:

```bash
export DIFF_CALLBACK_URL=http://host.docker.internal:8000/webhooks/diff
```

## Test heartbeats (start here)

Before wiring real registry events, confirm your agent receives signed webhooks:

```python
from streamgine import diff

diff.configure()  # after: source diff-streamgine-env.sh  OR  configure("diff-streamgine-credentials.json")
client = diff.DiffClient()
client.register_test(callback_url="https://your-agent.example/webhooks/diff")
```

Filter on **`event.is_test_heartbeat`** (or `after == {"test": "success"}`). The service sends this every second:

```json
{
  "action": "INSERT",
  "after": { "test": "success" }
}
```

No `entity_id` on test events.

## Quick start (production registry events)

```python
from streamgine import diff

diff.configure()
client = diff.DiffClient()
client.register(
    match={
        "state": "CA",
        "principal_city": "San Francisco",
    },
    actions=["INSERT"],
    callback_url="https://your-agent.example/webhooks/diff",
)
client.close()
```

`INSERT` = newly added company, `UPDATE` = changed, `DELETE` = removed. Omit `actions` to receive every matching state/city event.

### Search current companies (full story)

Webhooks deliver changes. Search returns **current** snapshots:

```python
from streamgine import diff

diff.configure()
client = diff.DiffClient()
result = client.search(
    query={"match": {"principalCity": "San Francisco"}},
    size=10,
)
for company in result["entities"]:
    print(company.get("entity_name"), company.get("entity_number"))
```

Agent journey: **Search → Watch → Qualify → Act → Stay current** (see [`/docs.md`](../../public/docs.md)).

### 2. Verify inbound webhooks (required)

Always verify **raw request bytes** before trusting JSON. The worker signs the exact JSON body with HMAC-SHA256.

```python
from streamgine import diff

diff.configure()

def handle_webhook(raw_body: bytes, signature: str | None):
    try:
        event = diff.verify_webhook(raw_body, signature)
    except diff.WebhookVerificationError:
        return 401, {"error": "invalid signature"}

    print(event.action, event.entity_id, event.after)
    return 200, {"ok": True}
```

**FastAPI**

```python
from fastapi import FastAPI, Header, Request, Response

from streamgine import diff

app = FastAPI()

@app.on_event("startup")
def setup() -> None:
    diff.configure()

@app.post("/webhooks/diff")
async def diff_webhook(
    request: Request,
    response: Response,
    x_diff_signature: str | None = Header(default=None, alias="X-Diff-Signature"),
):
    raw = await request.body()
    try:
        event = diff.verify_webhook(raw, x_diff_signature)
    except diff.WebhookVerificationError:
        response.status_code = 401
        return {"error": "invalid signature"}

    # Your agent logic here
    return {"ok": True, "entity_id": event.entity_id}
```

**Flask**

```python
from flask import Flask, request

from streamgine import diff

app = Flask(__name__)
diff.configure()

@app.post("/webhooks/diff")
def webhook():
    try:
        event = diff.verify_webhook(
            request.get_data(),
            request.headers.get("X-Diff-Signature"),
        )
    except diff.WebhookVerificationError:
        return {"error": "invalid signature"}, 401

    return {"ok": True, "action": event.action}
```

## Security

| Practice                     | How this client helps                                                                         |
| ---------------------------- | --------------------------------------------------------------------------------------------- |
| Verify every webhook         | `diff.verify_webhook()` uses `hmac.compare_digest` (timing-safe) on the raw body              |
| Match worker algorithm       | Same as Node: `HMAC-SHA256(secret, raw_json_bytes)` → header `X-Diff-Signature: sha256=<hex>` |
| Parse JSON only after verify | Tampered bodies fail before `WebhookEvent` is built                                           |
| HTTPS in production          | `DiffClient(verify_tls=True)` (default); use `https://` callback URLs                         |
| Keep secrets out of code     | One `configure()` from JSON or env — never scatter keys in constructors                       |

Never log the signing secret or skip signature verification in production.

### Credentials file (after checkout)

Download **`diff-streamgine-credentials.json`** (or **`diff-streamgine-env.sh`**) from `/account` / checkout. JSON format (version `1`):

```json
{
  "version": 1,
  "service": "diff.streamgine.com",
  "customer_id": "cus_…",
  "api_key": "de_live_…",
  "signing_secret": "whsec_…",
  "api_url": "https://diff.streamgine.com",
  "env": {
    "DIFF_API_KEY": "de_live_…",
    "DIFF_SIGNING_SECRET": "whsec_…",
    "DIFF_API_URL": "https://diff.streamgine.com"
  }
}
```

```python
from streamgine import diff

diff.configure("diff-streamgine-credentials.json")
client = diff.DiffClient()
# diff.verify_webhook(...) uses the same signing secret automatically
```

## Auth

| Env (via `configure()`) | Used for                                                        |
| ----------------------- | --------------------------------------------------------------- |
| `DIFF_API_KEY`          | `register()` / `register_test()` (customer Bearer)              |
| `DIFF_PROVIDER_API_KEY` | `get_state()` and provider ingest when provider auth is enabled |

Register responses are slim (`ok` / subscription fields only — no Redis index maps).

## API surface (`streamgine.diff`)

| Symbol                      | Role                                                       |
| --------------------------- | ---------------------------------------------------------- |
| `configure()`               | Load credentials once (JSON path or `DIFF_*` env)          |
| `get_config()`              | Read configured `ClientConfig`                             |
| `DiffClient`                | `register()`, `register_test()`, `health()`, `get_state()` |
| `verify_webhook()`          | Verify signature + return `WebhookEvent`                   |
| `DIFF_*` env name constants | `API_URL_ENV`, `API_KEY_ENV`, `SIGNING_SECRET_ENV`, …      |
| `WebhookEvent`              | Parsed `action`, optional `entity_id`, optional `after`    |
| `WebhookVerificationError`  | Invalid/missing signature or malformed payload             |
| `SIGNATURE_HEADER`          | `"X-Diff-Signature"`                                       |

Direct imports also work: `from streamgine.diff import DiffClient`.

## Development

```bash
cd clients/python
python -m pip install -e ".[dev]"
pytest
```
