Metadata-Version: 2.4
Name: everydeck
Version: 0.5.2
Summary: everydeck Python SDK: bounded buffer, background sender, flush on shutdown, reported data gaps
License-Expression: MIT
Project-URL: Homepage, https://everydeck.ai
Project-URL: Repository, https://github.com/everydeck/everydeck
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: jsonschema[format]>=4.26; extra == "test"
Requires-Dist: build>=1.2; extra == "test"
Requires-Dist: fastapi>=0.110; extra == "test"
Requires-Dist: httpx>=0.27; extra == "test"
Requires-Dist: openai>=1.40; extra == "test"
Requires-Dist: anthropic>=0.40; extra == "test"
Dynamic: license-file

# everydeck (Python SDK)

The Python server SDK of [everydeck](https://everydeck.ai): it reports the signals, errors and logs of your app's flows to everydeck.

```bash
pip install everydeck
```

```python
import os
from everydeck import Everydeck

everydeck = Everydeck(
    endpoint=os.environ["EVERYDECK_URL"],
    credential="<server key of the environment>",
    release_key="auto",
    manifest_digest="<digest of the approved manifest>",
    heartbeat_interval=300,
    instance_id="api",
)
everydeck.signal("job.created", operation_id)
```

- `signal()`, `error()` and `log()` never block and never raise: they enqueue into a bounded in-memory buffer; a background thread sends batches (at most 50 events, 64 KiB, gzip-compressed).
- Retries only on 429, 5xx, timeout and network errors; `Retry-After` is honoured; final rejections are dropped and counted, never reported as sent.
- A buffer overflow is reported as a `gap` event, so everydeck shows a known data gap instead of silence.
- `heartbeat_interval` sends `everydeck.sdk.heartbeat` from the sender thread. Give each component a stable `instance_id` (for example `"api"`, `"worker"`).
- `release_key="auto"` derives the version from the deployment commit (`EVERYDECK_RELEASE_KEY`, `GITHUB_SHA`, `VERCEL_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`, …) or local git.
- Server errors without a hand-written signal (0.5.0; FastAPI, Starlette, any ASGI app): `app.add_middleware(EverydeckMiddleware, everydeck=everydeck)` with `from everydeck.asgi import EverydeckMiddleware`. Every unhandled exception while answering a request (re-raised, the app answers as before) and every answer with status 500 to 599 becomes one `server.request_failed` event with method, route as a pattern (`/orders/:order_id`), status, duration, error class, the message scrubbed with `scrub_text`, frames and the run ids of a browser action. Never a body, a query, a cookie or any other header. 4xx is the app's answer and is not reported. Scrubbing runs in linear time and covers at most 8 192 characters (`SCRUB_INPUT_MAX`); a longer message is cut first and ends with `…`.
- Counts per type (0.5.0): the read bridge answers `GET /everydeck/read/counts` (rows per table of the read scope, `count(*)` read-only; `psycopg_count`, `PsycopgReadOnly.count`, `SupabaseReadOnly.count` with PostgREST `count=exact`) and names `count: true` per type, so everydeck's data loss watch does not wait for a full read of a large table. Update everydeck first.
- A date column as a calendar day (0.5.0): the read bridge sends a `date` (also inside a list) as midnight UTC of that day (`2026-10-19T00:00:00Z`), like the TypeScript SDK, in every time zone of the server. Before, it sent `2026-10-19` without a time, which everydeck does not take for a date field. Timestamps (`datetime`) are unchanged.
- When a record was created (0.5.2): the read bridge also delivers `created_at` (ISO) from the column the database sets when a record is created (`created` in the read scope, per object type and for people), so everydeck can show the app's past from before it was connected.
- Several refunds and failed attempts of one order (0.5.1): `everydeck.payment("order_refunded", order_id=..., refund_id=...)` with the provider's id of this one refund (Stripe `re_…`) and `payment_failed` with `attempt_id` (Stripe `ch_…`): two partial refunds of 20 and 30 on 100 count as 50, retries count each, a refund delivered twice counts once. Without these ids everything stays as before. `provider=` (the name in money.providers of the app map, such as `stripe`) says which provider a payment came from. Update everydeck first.
- Image and audio calls (0.5.1): `wrap_openai` also measures `images.generate`, `images.edit`, `audio.transcriptions.create`, `audio.translations.create` and `audio.speech.create` (operation `image_generation`, `audio_transcription`, `audio_speech`). everydeck's price list prices text tokens only; these calls show their cost as unknown unless you set a price of your own. Update everydeck first.
- Payments (0.5.0, server only): `everydeck.payment(event, order_id=..., total=..., currency=..., product=..., quantity=..., checkout_id=..., reason=..., subject_ref=..., account_ref=..., object_type=..., object_ref=...)` where the app itself talks to its payment provider, in the code that opens a checkout and in the webhook it receives (for example a FastAPI route `/webhooks/stripe`). The same five events and fields as the TS SDK, named after the Segment e-commerce spec and Stripe: `checkout_started`, `order_completed`, `payment_failed`, `order_refunded`, `checkout_abandoned`. `total` is in the main unit of the currency (divide Stripe's cents by 100, except for zero decimal currencies), `currency` an ISO 4217 code; `order_id` (or `checkout_id` for a checkout) is required and counts once per event, so a webhook delivered twice is safe. When the webhook stores the purchase as a row of a purchases table of the app map, pass that row's id as `order_id` (several rows for one payment: the first row's id and `quantity` the number of rows), so everydeck counts the purchase once. Never card data: an event with a value that looks like a card number is not sent. No key of the provider ever goes to everydeck. Update everydeck first.
- AI calls (0.5.0, server only): `openai_client = everydeck.wrap_openai(OpenAI())` and `everydeck.wrap_anthropic(Anthropic())` (also `AsyncOpenAI`, `AsyncAnthropic`) return the same client (`isinstance` stays true), like PostHog's `posthog.ai.openai` and Langfuse's `langfuse.openai`. The same event `everydeck.ai.generation` and fields as the TS SDK (OpenTelemetry GenAI names: model, tokens in and out with cached tokens, duration, `error.type`, `everydeck.flow`). Never the prompt, the answer, a system text or a key. Name the flow, the person and the organization at the wrapper (`wrap_openai(client, flow="cv_analysis", subject_ref=..., account_ref=...)`) or around the work (`with everydeck.ai_context(flow="cv_analysis", account_ref=org_id):`; the calls inside belong to one run). `everydeck.ai_call(provider=..., request_model=..., input_tokens=..., output_tokens=...)` sends a call the app measured itself. everydeck prices the tokens; the app sends no money and no key of the provider. Update everydeck first.
- Sending time (0.5.0): every batch carries `X-Everydeck-Sent-At`, so everydeck can correct a server clock that is off by an hour or more (like Sentry's `sent_at`); `error()` takes `session_id`.
- Settings read again while running (0.5.0): `Everydeck.from_supabase` reads `public.everydeck_server_config()` again every 5 minutes (`refresh_seconds`, 0 = only at start), the way PostHog polls its flag definitions. A new approval in everydeck (manifest digest) or a new server key reaches a long-running server without a restart; batches built after the read carry the new values. A failed read keeps the last settings (one warning that names the function, never a value). A client switched off at start because the function was not there yet switches itself on once the values arrive. Environment variables stay fixed.
- A server next to a hosted Supabase project (0.4.1): `Everydeck.from_supabase(supabase_admin, instance_id="api", heartbeat_interval=300)` reads endpoint, server key and manifest digest once at start from `public.everydeck_server_config()` through the server's own Supabase client (service key). everydeck adds that function after the owner's approval; only `service_role` may call it. Nothing to copy into the hosting dashboard. When `EVERYDECK_ENDPOINT`, `EVERYDECK_SERVER_KEY` and `EVERYDECK_MANIFEST_DIGEST` are all set, they win. If the call fails, the SDK switches itself off with a warning that names the function, never a value.
- `everydeck.verify.verify_everydeck_request` checks everydeck's signature on bridge calls.
- Read bridge (0.5.0, pull): `from everydeck.read_route import mount_read_bridge, SupabaseReadOnly` and `mount_read_bridge(app, SupabaseReadOnly(supabase_admin))` in the module that creates `app` start a background thread that fetches the bridge's requests from everydeck with a long poll (`GET <EVERYDECK_URL>/v1/read-bridge/next`) and sends the answers back, outbound only, like a CI runner: no route on the app, everydeck needs no address. Every request it answers must carry everydeck's signature. Other frameworks call `start_read_bridge(db)` once at start. It reads `EVERYDECK_URL` and `EVERYDECK_READ_BRIDGE_TOKEN` from the environment and waits while they are not set.
- Read bridge (0.3.0): `everydeck.read_route.EverydeckReadBridge(endpoint, key, db)` is the ready-made route for other frameworks. It fetches its read scope from everydeck (`GET <endpoint>/v1/read-scope` with the bridge key) and uses it only with everydeck's signature; no read-scope file, no read role, no views. `db` is the connection the app already has: `PsycopgReadOnly(conninfo)` runs every query in `BEGIN READ ONLY`, `SupabaseReadOnly(client)` reads through the app's Supabase client with `select()` only (GET requests, which PostgREST runs read-only; only tables in schemas the Supabase API exposes).

No dependencies. Python 3.11 or newer. License: MIT.
