Metadata-Version: 2.4
Name: signalgate
Version: 0.2.0
Summary: SignalGate antifraud backend SDK for Python — thin HTTP forwarder to api.signalgate.ai
Project-URL: Homepage, https://signalgate.ai
Project-URL: Documentation, https://signalgate.ai/docs/python
Author: SignalGate
License: Apache-2.0
License-File: LICENSE
Keywords: antifraud,fingerprint,sdk,signalgate
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: types-requests; extra == 'dev'
Description-Content-Type: text/markdown

# signalgate — Python backend SDK

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)

Thin HTTP forwarder for [SignalGate](https://signalgate.ai) antifraud.
Tenants embed this in their backend to call `/v0/check` (synchronous verdict)
and `/v0/log` (async analytics). The SDK does **no crypto** — the opaque
encrypted payload comes from `@signalgate/frontend-js-sdk`.

## Install

```bash
pip install signalgate
```

For local development against an unpublished checkout:

```bash
pip install -e /path/to/backend-python-sdk
```

## Quickstart

> **Rule:** call `check` OR `log` for a given event, never both. As of v0.2.0
> the server persists the event as part of `/v0/check`, so a follow-up `log()`
> with the same nonce is replay-rejected (422). Choose one path per event.

**Path A — you need a verdict (call `check` only):**

```python
from signalgate import Client, Event, EncryptedPayload

client = Client(api_key="<your-jwt>")

event = Event(
    user_id="u_123",
    ip=request.headers.get("X-Forwarded-For", request.remote_addr),
    method="login",
    timestamp="2026-04-01T13:08:50+00:00",
    payload=EncryptedPayload(**frontend_payload_dict),  # verbatim from the browser
    custom={"plan": "pro"},
)

# check() sends to /v0/check, which returns a verdict AND persists the event
# server-side for analytics. Do NOT call log() afterwards.
verdict = client.check(event)
if verdict.action == "block":
    raise PermissionError("blocked by SignalGate")
```

**Path B — fire-and-forget analytics only (no verdict needed):**

```python
# log() sends to /v0/log asynchronously. Use this for events you are not
# checking — background actions, page-view telemetry, etc.
client.log(event)
```

At process shutdown:

```python
client.close()  # drains the log queue within 5 × log_timeout_ms
```

Or use it as a context manager:

```python
with Client(api_key="...") as client:
    client.check(event)
```

## Configuration

Override the defaults selectively:

```python
Client(
    api_key="...",
    check_timeout_ms=3000,
    log_timeout_ms=1000,
    log_queue_capacity=10000,
    log_max_retries=3,
    fail_open=True,   # default: on timeout/5xx, check() returns allow
)
```

## Error handling

`check()` raises on 4xx (tenant bug — bad JWT, malformed event). With
`fail_open=True` (default), timeouts / network errors / 5xx return a
synthesized `CheckResult(action="allow", failed_open=True)`.

```python
from signalgate import ServerError, TimeoutError, NetworkError

try:
    result = client.check(event)
except ServerError as e:
    # e.status_code, e.code, e.message, e.request_id, e.details
    ...
```

`log()` never raises. Failures are counted in `client.metrics`.

## Metrics

```python
client.metrics.get("check_total")
client.metrics.get("check_failed_open_total")
client.metrics.get("log_dropped_total", reason="queue_full")
client.metrics.snapshot()  # all counters
```

Full counter list:

| Counter | Labels | Incremented when |
|---|---|---|
| `check_total` | — | `check()` called |
| `check_success_total` | — | `check()` returned a real verdict |
| `check_failed_open_total` | — | `check()` returned a synthesized allow |
| `check_error_total` | `type` | `check()` raised |
| `log_enqueued_total` | — | `log()` accepted into queue |
| `log_sent_total` | — | server acknowledged a log event |
| `log_http_error_total` | `status` | a log retry was triggered |
| `log_dropped_total` | `reason` | `queue_full` / `closed` / `retry_exhausted` |

## Development

```bash
pip install -e ".[dev]"   # install with dev extras
pytest -q                 # run the test suite
```

CI runs `pytest -q` on every push and PR to `main` via `.github/workflows/test.yml` (Python 3.12, pip cache keyed on `pyproject.toml`).

## License

Licensed under the [Apache License, Version 2.0](LICENSE).

## Links

- Homepage: <https://signalgate.ai>
- Documentation: <https://signalgate.ai/docs/python>
