Metadata-Version: 2.5
Name: xlambda-core
Version: 0.2.0
Summary: Shared HTTP client and webhook verification for xlambda's service SDKs (xlambda-media, xlambda-postgres, xlambda-redis, xlambda-email).
Project-URL: Homepage, https://xlambda.tech
Project-URL: Source, https://github.com/randyryan177-cloud/media-server/tree/main/packages-python/core
Author: xlambda
License-Expression: MIT
Keywords: sdk,webhooks,xlambda
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == 'fastapi'
Provides-Extra: flask
Requires-Dist: flask>=2.2; extra == 'flask'
Description-Content-Type: text/markdown

# xlambda-core

Shared HTTP client, error handling, and webhook verification/parsing for
xlambda's service SDKs. A dependency of the other four — install
[`xlambda-media`](../media), [`xlambda-postgres`](../postgres),
[`xlambda-redis`](../redis), or [`xlambda-email`](../email) instead, unless
you're building a generic multi-event-family webhook receiver.

```
pip install xlambda-core
```

This is the Python port of [`@xlambda-tech/core`](../../packages/core) and
wraps the same `/v1/*` API. Same wire format, same error envelope, same
webhook signature scheme — a receiver can be moved between the two without
touching the dashboard's webhook config.

## Two clients

Every package here ships a blocking client and an awaitable one, sharing one
definition of retry policy and error mapping:

```python
from xlambda.core import XlambdaClient, AsyncXlambdaClient

with XlambdaClient(api_key=..., base_url="https://api.xlambda.tech") as client:
    project = client.request("/v1/things", query={"projectId": "p1"})

async with AsyncXlambdaClient(api_key=...) as client:
    project = await client.request("/v1/things", query={"projectId": "p1"})
```

Retries 429 (honoring `X-RateLimit-Reset` when present) and 5xx with
exponential backoff and jitter, up to `max_retries` (default 3). Never
retries a 4xx — that's the caller's own mistake and retrying won't change
the outcome. Every non-2xx raises `XlambdaError` with `.code`, `.status`,
`.message`, `.details` read from the platform's documented error envelope.

Unlike the npm SDK (where `fetch` has no default timeout), these default to
a 30s per-request `timeout` — httpx's own 5s default is too tight for
uploads. Pass your own `http_client` to control proxies, TLS, or transports.

## Webhooks

`handle(raw_body, signature_header)` is the framework-agnostic core; every
adapter is a thin wrapper around it.

```python
from xlambda.core import create_webhook_handler

handler = create_webhook_handler(
    secret=os.environ["XLAMBDA_WEBHOOK_SECRET"],
    on_event=lambda event: print(event["event"], event["data"]),
)

result = handler.handle(raw_bytes, signature_header)  # -> .status, .body
```

Ready-made servers, no framework needed:

```python
handler.to_wsgi_app()                                    # sync
create_async_webhook_handler(...).to_asgi_app()          # async
```

Framework adapters live behind their own imports, so installing this package
never pulls in a web framework you don't use:

```python
# pip install "xlambda-core[flask]"
from xlambda.core.webhooks.adapters.flask import create_flask_blueprint
app.register_blueprint(create_flask_blueprint(handler), url_prefix="/webhooks")

# pip install "xlambda-core[fastapi]"
from xlambda.core.webhooks.adapters.fastapi import create_fastapi_router
app.include_router(create_fastapi_router(async_handler), prefix="/webhooks")
```

**Always verify the raw request bytes.** Both adapters read the body before
anything parses it (`request.get_data()` / `await request.body()`). A
receiver that verifies a re-serialized body will fail every signature, since
the bytes no longer match what was signed.

An event that doesn't match your `filter` still gets a 200 with
`{"received": true, "handled": false}` — the platform treats anything but a
2xx as a delivery failure and retries three times, so "not interested in this
event" has to look like success on the wire. An exception raised inside
`on_event` deliberately propagates, so your framework turns it into a 5xx and
the delivery is retried.

## Conventions

- **Inputs are snake_case, outputs are the API's own camelCase.** Method
  arguments read as Python (`project_id=...`); responses are returned as the
  raw decoded JSON (`domain["dkimRecords"]`), typed by `TypedDict`. That keeps
  responses 1:1 with `docs/API.md` and means a field the server adds tomorrow
  arrives intact instead of being dropped by a rename table this version has
  never heard of.
- **`from` is spelled `from_` as an argument**, since it's a Python keyword —
  but stays `from` in responses and webhook payloads, where it's just a dict
  key.
- Responses are plain dicts at runtime. `TypedDict` is a type-checker
  construct only; nothing is validated or coerced.

## Development

```
pip install -e ".[flask,fastapi]" pytest
pytest
```

The test suite includes a cross-language golden vector — a signature
generated by Node's own `crypto` against `webhookWorker.ts`'s exact
construction — so a pass proves this verifier accepts what the live platform
actually sends, not just what this package signs.
