Metadata-Version: 2.4
Name: everydeck
Version: 0.5.8
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.
- The read bridge says why instead of leaving everydeck waiting (0.5.8): each query of `PsycopgReadOnly` runs at most 20 s (`statement_timeout`) and waits at most 5 s for a lock (`lock_timeout`) in its read-only transaction, under everydeck's 30 s; a table locked by another session answers `500` with "another session holds a lock on the table (SQLSTATE 55P03)", a query over its time with SQLSTATE 57014. In pull mode (`pull_once`) a failed query becomes a `500` answer instead of an exception, and an answer everydeck refuses (larger than it takes, 413) is reported as `502` with its size. everydeck reads such a type with smaller pages from where it stopped.
- The server names its public address (0.5.7): the heartbeat of an instance carries the address its host sets (`RENDER_EXTERNAL_URL` at Render, `RAILWAY_PUBLIC_DOMAIN` at Railway, `HEROKU_APP_DEFAULT_DOMAIN_NAME` at Heroku), and everydeck sets up its reachability check there by itself. Nothing is guessed: on any other host and in job heartbeats nothing is sent (`everydeck.host_public_url()` shows what would be sent).
- Python 3.11 again (0.5.6): 0.5.5 stopped at import on Python 3.11 (`SyntaxError: f-string expression part cannot include a backslash`), so an app on 3.11 did not start. 0.5.6 imports and passes its tests on Python 3.11, the oldest version it supports, and on the newest. Business keys of payment events are unchanged.
- Read bridge (0.5.5): a quiet bridge sends almost nothing. everydeck asks once per read whether anything changed, like HTTP `ETag` / `If-None-Match` with `304 Not Modified`: `GET /everydeck/read/etags?limit=<page size>` answers one fingerprint per type and for the people (the pages a full read would deliver now, folded in order, with their count), and everydeck fetches only the types whose fingerprint changed. A page carries its own `etag` only when everydeck asks (`etag=1`), so an older everydeck sees exactly the pages and capabilities it knows. The bridge still reads the rows to form the fingerprints (read-only, as for a full read); a type with more than 50,000 rows gets no fingerprint and is read in full as before, the types after it keep theirs; past 5 s for the whole request the rest is read in full. Update everydeck first.
- Read bridge (0.5.4): a new person is in everydeck within seconds. The bridge answers `GET /everydeck/read/people/{subject_ref}` with exactly this person (404 when the app does not know the identifier, also one its key column cannot hold) and names `people.fetch_one: true`; everydeck asks right after an event names someone it does not know yet, instead of waiting for the next read every 10 minutes. With 0.5.3 and older, everydeck reads the first pages of the people list instead.
- Read bridge (0.5.3): one pull waits up to 50 s (before 25 s, like GitHub's runners; `PULL_WAIT_S`), so a bridge with nothing to do asks everydeck about 72 times an hour instead of 144. Requests still arrive at once. An older everydeck caps the wait at 25 s itself.
- 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.
