Metadata-Version: 2.5
Name: sigtake
Version: 0.2.0
Summary: Official Python SDK for Sigtake — alert and signal ingestion.
Project-URL: Homepage, https://sigtake.com
Project-URL: Documentation, https://docs.sigtake.com
Project-URL: Source, https://github.com/Sigtake/python-sdk
Project-URL: Changelog, https://github.com/Sigtake/python-sdk/blob/main/CHANGELOG.md
Author-email: Sigtake <help@sigtake.com>
License: MIT
License-File: LICENSE
Keywords: alerting,incidents,monitoring,observability,sigtake
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/Sigtake/python-sdk/main/assets/logo.png" alt="Sigtake" width="72" height="72" />
</p>

# sigtake

**English** · [Español](https://github.com/Sigtake/python-sdk/blob/main/README.es.md)

Official Python SDK for [Sigtake](https://sigtake.com). Sends alerts and signal readings. Sync and
async, one dependency, typed.

```bash
pip install sigtake
```

Requires Python 3.10 or newer.

## Quickstart

```python
from sigtake import Sigtake

with Sigtake(api_key="sk_...") as sigtake:
    sigtake.alerts.ingest(
        title="Checkout latency above threshold",
        severity="high",
        source="checkout-api",
        team_code="OPS",
        payload={"p95_ms": 2400, "region": "eu-west-1"},
    )
```

Without `api_key`, the client reads `SIGTAKE_API_KEY` from the environment.

## Getting an API key

In the app: **API Keys → New Key** (requires `admin` or `super_admin`). The key is shown **once** —
copy it then.

The key resolves both your tenant and your project. There is no project id to pass: one key per
project, and staging and production keys are different keys.

The `team_code` comes from **Teams**, printed on each team card. It decides who gets notified.

## Async

Same surface, with `await`. Use it in FastAPI or any asyncio app: the sync client would block the
event loop on every alert.

```python
from sigtake import AsyncSigtake

async with AsyncSigtake() as sigtake:
    await sigtake.alerts.ingest(title="Payment declined", source="checkout", team_code="PAY")
```

Create one client per process and reuse it — that is what makes the connection pool worth having.

## Alerts

```python
result = sigtake.alerts.ingest(
    title="Payment webhook failing",
    source="payments-worker",
    team_code="PAY",
    severity="critical",  # 'critical' | 'high' | 'medium' | 'low' | 'info', defaults to 'info'
    payload={"attempt": 3},  # optional, must serialize to 8 KB or less
)

result.data.id  # alert id
result.meta.is_duplicate  # True when it folded into an open incident
result.meta.occurrence_count  # how many times this incident has fired
```

The response never echoes `payload` back. On a dedup hit the alert you get is the pre-existing
one, and its payload may have been written by a different sender.

Alerts are deduplicated on `source | title | severity` per project. An open alert with the same
fingerprint gets its occurrence count bumped instead of opening a second incident, and notifications
go out at occurrences 1, 10, 25, 50, 100, then every 100.

`team_code` must exist in your tenant **and** the team must be assigned to the project this API key
belongs to. If it isn't, the call fails with `code == "TEAM_NOT_IN_PROJECT"` — assign the team to
the project in the app.

## Signals

```python
# Flat dict — the common case
sigtake.signals.send("billing-service", {"emails_sent": 42, "queue_depth": 3})

# Explicit readings
sigtake.signals.ingest(
    source="billing-service",
    readings=[{"metric": "emails_sent", "value": 42}],
)
```

`source` is the monitor's source key, which must already exist in the project. Up to 100 readings
per call, 50 distinct metric names per monitor.

Hitting the metric cap answers with a **partial delivery** rather than an error — the readings that
fit were stored:

```python
result = sigtake.signals.send("billing-service", metrics)
if result.rejected:
    logger.warning("metric cap reached, dropped: %s", result.rejected)
```

A total rejection (nothing stored) raises `SigtakeValidationError`.

## Errors

Every failure is a `SigtakeError` subclass carrying `status`, `code` and the raw `body`.

```python
from sigtake import SigtakeRateLimitError, SigtakeValidationError

try:
    sigtake.alerts.ingest(title=..., source=..., team_code=...)
except SigtakeValidationError as err:
    print(err.field_errors)
except SigtakeRateLimitError:
    print("retries exhausted")
```

| Class | When |
| --- | --- |
| `SigtakeValidationError` | 400, the total-rejection 422, and local input checks (`status == 0`) |
| `SigtakeAuthError` | 401 / 403 — missing, invalid or disabled key |
| `SigtakeNotFoundError` | 404 — no monitor with that source in this project |
| `SigtakeConflictError` | 409 — monitor is paused |
| `SigtakeRateLimitError` | 429, after retries are exhausted |
| `SigtakeServerError` | 5xx, after retries are exhausted |
| `SigtakeNetworkError` | DNS, connection, or timeout (`status == 0`) |

408, 429, 5xx and network failures are retried with exponential backoff and jitter. Validation
errors never are.

> **No idempotency.** The API does not accept an idempotency key yet, and timeouts *are* retried.
> If a request times out after reaching the server, the retry bumps `occurrence_count` on the
> existing alert. Nothing is lost or duplicated as an incident, but a notification threshold may
> fire slightly early.

## Configuration

```python
Sigtake(
    api_key="sk_...",  # or SIGTAKE_API_KEY
    base_url="https://api.sigtake.com",  # default; or SIGTAKE_BASE_URL
    timeout=10.0,  # seconds, per attempt
    max_retries=3,  # 0 disables retries
    headers={},  # merged into every request
    http_client=None,  # bring your own httpx.Client; if you pass it, you close it
)
```

Per call:

```python
sigtake.alerts.ingest(title=..., source=..., team_code=..., timeout=2.0)
```

Alert ingestion is best treated as fire-and-forget — never let it break the code path that produced
the alert:

```python
try:
    sigtake.alerts.ingest(title=..., source=..., team_code=...)
except SigtakeError:
    logger.warning("sigtake ingest failed", exc_info=True)
```

## Development

```bash
uv sync --all-groups
uv run ruff check . && uv run mypy
uv run pytest tests/unit --cov=sigtake
```

Contract tests need a mock served from the spec:

```bash
docker run --rm -p 4010:4010 -v "$PWD/spec:/spec" stoplight/prism:5 \
  mock -h 0.0.0.0 --errors -m false /spec/openapi.yaml
uv run pytest tests/contract
```

More detail in
[`docs/ARCHITECTURE.md`](https://github.com/Sigtake/python-sdk/blob/main/docs/ARCHITECTURE.md).

## Releasing

```bash
git tag sdk-python-v0.1.0 && git push --tags
```

The tag triggers the publish workflow, which refuses to publish if the tag and `pyproject.toml`
disagree.

## License

MIT
