Metadata-Version: 2.4
Name: source-identity-http
Version: 0.1.0.dev2
Summary: Typed, zero-dependency caller-identity and trace headers (X-Source-ID, X-Trace-ID, ...) for httpx, requests, and any HTTP client with .headers
Author: Powertica / Biztrade
License: MIT
Keywords: headers,httpx,microservices,observability,requests,service-identity,tracing,x-request-id,x-source-id,x-trace-id
Classifier: Programming Language :: Python :: 3
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: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pylint>=3.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: requests>=2.31; extra == 'dev'
Description-Content-Type: text/markdown

# source-identity-http

Tiny, **zero-dependency** (stdlib only), **typed** helpers to attach caller-identity and distributed-trace headers — `X-Source-ID`, `X-Source-Version`, `X-Source-Team`, `X-Trace-ID`, `X-Request-ID` — to outbound HTTP requests made with `httpx`, `requests`, or **any** client that exposes a mutable `.headers` mapping.

- **No runtime deps.** Pure stdlib. Pulls in nothing.
- **Typed.** `py.typed` marker, strict-mypy clean, `Final`/`Protocol`/frozen `dataclass` throughout.
- **Library-agnostic.** Works with `httpx` (sync + async), `requests`, `urllib3`-based clients, or anything with `.headers.update(...)`.
- **Three ergonomics.** Build a raw `dict`, merge into call kwargs, or set defaults on a session/client.

## Why

In a microservice architecture, knowing **who** called **what**, and **which request** that call belongs to, is the difference between a five-minute incident and a five-hour one. The conventions for surfacing this on the wire are well established:

| Header             | Meaning                                                  |
| ------------------ | -------------------------------------------------------- |
| `X-Source-ID`      | Name of the calling service (e.g. `order-service`)       |
| `X-Source-Version` | Version of the calling service (e.g. `2.1.0`)            |
| `X-Source-Team`    | Team or org owning the caller (e.g. `platform`)          |
| `X-Trace-ID`       | Correlation id propagated across hops                    |
| `X-Request-ID`     | Same as `X-Trace-ID` here (sent for compatibility)       |

This package gives you a single, typed way to set them — without each service hand-rolling its own header plumbing, and without dragging in a heavy observability framework.

It plays well with:

- **Reverse proxies / call trackers** (e.g. Traefik with a call-tracker middleware) that aggregate `X-Source-*` to draw service-dependency graphs.
- **APM / log pipelines** that group logs by `X-Trace-ID` / `X-Request-ID`.
- **Anything** that just wants a consistent "who is calling" header on every outbound call.

## Install

```bash
pip install source-identity-http
```

Requires **Python 3.10+**. No runtime dependencies.

## Usage

### 1. One-shot calls — `merge_http_kwargs`

Merges identity headers into the `headers=` slot of any call-style kwargs dict. Safe to use with both `httpx` and `requests` module-level helpers:

```python
import httpx, requests
from source_identity_http import SourceIdentity, merge_http_kwargs

identity = SourceIdentity(
    source_id="order-service",
    version="2.1.0",
    team="platform",
    trace_id="abc-123",
)

r = httpx.get("https://api.example/v1", **merge_http_kwargs({}, identity))

r = requests.post(
    "https://api.example/v1",
    **merge_http_kwargs({"json": {"foo": "bar"}}, identity),
)
```

Any existing `headers=` you pass in is preserved; identity headers are merged **on top** (the identity wins on key conflicts — that's intentional, so a caller can't accidentally spoof its own source id).

### 2. Default headers on a long-lived client — `apply_default_headers`

For `httpx.Client` / `httpx.AsyncClient` / `requests.Session`, set the headers once and forget:

```python
import httpx
from source_identity_http import SourceIdentity, apply_default_headers

identity = SourceIdentity(source_id="billing-svc", team="risk")

async with httpx.AsyncClient() as client:
    apply_default_headers(client, identity)
    await client.get("https://upstream/health")
    await client.get("https://upstream/invoices")
    # Both requests carry the identity headers.
```

Per-request `headers=` on individual calls still merge on top in the usual library-specific way.

### 3. Raw header `dict` — `build_headers`

For everything else (custom transport, `urllib`, ASGI test clients, an SDK that doesn't take a session, …):

```python
from source_identity_http import SourceIdentity, build_headers

headers = build_headers(
    SourceIdentity(source_id="search-api", trace_id="t-9f2a"),
    extra={"Authorization": "Bearer ..."},
)
# headers -> {
#     "X-Source-ID": "search-api",
#     "X-Trace-ID":  "t-9f2a",
#     "X-Request-ID": "t-9f2a",
#     "Authorization": "Bearer ...",
# }
```

`extra` is merged **last**, so it can override non-required headers if you really need to (don't override `X-Source-ID` unless you know what you're doing).

## API

| Symbol                  | Role                                                                       |
| ----------------------- | -------------------------------------------------------------------------- |
| `SourceIdentity`        | Frozen dataclass: `source_id` (required), optional `version`, `team`, `trace_id` |
| `build_headers`         | Returns a `dict[str, str]` for `headers=`                                  |
| `merge_http_kwargs`     | Copies a kwargs mapping and merges identity into its `headers` slot        |
| `apply_default_headers` | Mutates `client.headers` so every request includes identity headers        |
| `SupportsDefaultHeaders`| `Protocol` describing clients with a mutable `.headers` attribute          |
| `HEADER_SOURCE_ID`      | `"X-Source-ID"`                                                            |
| `HEADER_SOURCE_VERSION` | `"X-Source-Version"`                                                       |
| `HEADER_SOURCE_TEAM`    | `"X-Source-Team"`                                                          |
| `HEADER_TRACE_ID`       | `"X-Trace-ID"`                                                             |
| `HEADER_REQUEST_ID`     | `"X-Request-ID"`                                                           |

`trace_id`, when set, is emitted as **both** `X-Trace-ID` and `X-Request-ID` — different upstreams pick one or the other, and you almost always want both.

## Compatibility

Tested against `httpx >= 0.27` and `requests >= 2.31`. The implementation only assumes:

- `client.headers` exists and has an `update(mapping)` method (for `apply_default_headers`), **or**
- the HTTP library accepts a `headers=` kwarg that is a plain `dict[str, str]` (for `build_headers` / `merge_http_kwargs`).

That covers essentially every popular Python HTTP client.

## Layout

```
src/source_identity_http/
├── __init__.py   # public re-exports
├── core.py       # SourceIdentity dataclass + build_headers
├── kwargs.py     # merge_http_kwargs
├── client.py     # apply_default_headers + SupportsDefaultHeaders protocol
└── py.typed      # PEP 561 marker
```

## License

MIT.
