|
| 1 | +# Runtime Context User Guide |
| 2 | + |
| 3 | +## How it works |
| 4 | + |
| 5 | +The runtime context lets SDK modules read caller-identity information (tenant, |
| 6 | +user, trigger type) for the current execution — without knowing where that |
| 7 | +information came from or what framework is running. |
| 8 | + |
| 9 | +- **`bootstrap(app)`** wires the SDK into your framework once at startup. |
| 10 | +- **Providers** extract context from the current invocation (HTTP request, gRPC call, Kubernetes event, etc.). |
| 11 | +- **`get_context()`** lets any module read that context via typed keys. |
| 12 | + |
| 13 | +``` |
| 14 | +bootstrap(app) |
| 15 | + └─ registers middleware on your framework |
| 16 | + └─ on each invocation: providers extract → RequestContext set in ContextVar |
| 17 | + └─ anywhere: get_context().get(TENANT_ID) |
| 18 | +``` |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## Quick start |
| 23 | + |
| 24 | +### 1. Bootstrap at app startup |
| 25 | + |
| 26 | +```python |
| 27 | +from starlette.applications import Starlette |
| 28 | +from sap_cloud_sdk import bootstrap |
| 29 | + |
| 30 | +app = Starlette(...) |
| 31 | +bootstrap(app) |
| 32 | +``` |
| 33 | + |
| 34 | +By default `bootstrap` registers `IASContextProvider` (reads IAS JWT) and |
| 35 | +`HeaderContextProvider` (reads SAP standard headers like `x-sap-origin`). |
| 36 | + |
| 37 | +### 2. Read context anywhere |
| 38 | + |
| 39 | +```python |
| 40 | +from sap_cloud_sdk.core.runtime_context import get_context, TENANT_ID, USER_ID, TRIGGER_TYPE |
| 41 | + |
| 42 | +ctx = get_context() |
| 43 | +ctx.get(TENANT_ID) # -> "abc-123" or None |
| 44 | +ctx.get(USER_ID) # -> "user-uuid" or None |
| 45 | +ctx.get(TRIGGER_TYPE) # -> "ui5" or None |
| 46 | +``` |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## Context keys |
| 51 | + |
| 52 | +Values are stored and retrieved by typed `ContextKey` instances — not strings. |
| 53 | +Each provider owns the keys it defines. Import keys from the provider that |
| 54 | +defined them. |
| 55 | + |
| 56 | +```python |
| 57 | +# IAS-owned keys: |
| 58 | +from sap_cloud_sdk.core.runtime_context import TENANT_ID, USER_ID, IAS_CLAIMS |
| 59 | + |
| 60 | +# SDK-standard keys (not tied to any specific source): |
| 61 | +from sap_cloud_sdk.core.runtime_context import TRIGGER_TYPE |
| 62 | + |
| 63 | +# Define your own: |
| 64 | +from sap_cloud_sdk.core.runtime_context import ContextKey |
| 65 | + |
| 66 | +MY_KEY = ContextKey[str]("my_key") |
| 67 | +``` |
| 68 | + |
| 69 | +Keys are identity-based — two `ContextKey("same_name")` instances are different |
| 70 | +keys. Always import the key from the module that defined it. |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## Providers |
| 75 | + |
| 76 | +A provider extracts a `RequestContext` from a `RequestEnvelope` — a |
| 77 | +framework-agnostic carrier of whatever signals were available at invocation time |
| 78 | +(headers, body, metadata). The provider doesn't know which framework built the |
| 79 | +envelope; the framework adapter doesn't know what the provider does with it. |
| 80 | + |
| 81 | +This means providers are reusable across transports. An `IASContextProvider` |
| 82 | +written for HTTP headers works identically if the same headers appear in gRPC |
| 83 | +metadata or a message queue envelope — as long as the adapter populates |
| 84 | +`RequestEnvelope.headers` consistently. |
| 85 | + |
| 86 | +### Built-in providers |
| 87 | + |
| 88 | +| Provider | Reads | Sets | |
| 89 | +|---|---|---| |
| 90 | +| `IASContextProvider` | `Authorization: Bearer <JWT>` | `TENANT_ID`, `USER_ID`, `IAS_CLAIMS` | |
| 91 | +| `HeaderContextProvider` | `x-sap-origin` | `TRIGGER_TYPE` | |
| 92 | + |
| 93 | +### Custom providers |
| 94 | + |
| 95 | +```python |
| 96 | +from sap_cloud_sdk.core.runtime_context import ( |
| 97 | + ContextKey, ContextProvider, RequestContext, RequestEnvelope |
| 98 | +) |
| 99 | + |
| 100 | +CORRELATION_ID = ContextKey[str]("correlation_id") |
| 101 | + |
| 102 | +class CorrelationIdProvider(ContextProvider): |
| 103 | + def extract(self, envelope: RequestEnvelope) -> RequestContext: |
| 104 | + value = envelope.headers.get("x-correlation-id") |
| 105 | + return RequestContext({CORRELATION_ID: value} if value else {}) |
| 106 | +``` |
| 107 | + |
| 108 | +Pass it to `bootstrap`: |
| 109 | + |
| 110 | +```python |
| 111 | +from sap_cloud_sdk.core.runtime_context import IASContextProvider, HeaderContextProvider |
| 112 | + |
| 113 | +bootstrap(app, providers=[IASContextProvider(), HeaderContextProvider(), CorrelationIdProvider()]) |
| 114 | +``` |
| 115 | + |
| 116 | +### Merging |
| 117 | + |
| 118 | +When multiple providers are registered, their results are merged — first writer |
| 119 | +wins per key. Providers that set different keys don't interfere with each other. |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +## Framework adapters |
| 124 | + |
| 125 | +`bootstrap` auto-detects the framework from the `app` type via registered |
| 126 | +`FrameworkAdapter` instances. Each adapter knows how to intercept invocations |
| 127 | +for one framework and build a `RequestEnvelope` from whatever the framework |
| 128 | +exposes. Adding support for a new framework or invocation source never requires |
| 129 | +editing `bootstrap`. |
| 130 | + |
| 131 | +### Currently supported |
| 132 | + |
| 133 | +| Framework | Detected via | |
| 134 | +|---|---| |
| 135 | +| Starlette / FastAPI | `isinstance(app, Starlette)` | |
| 136 | + |
| 137 | +### Adding a new framework or invocation source |
| 138 | + |
| 139 | +```python |
| 140 | +from sap_cloud_sdk.core.runtime_context import ContextProvider, FrameworkAdapter, register |
| 141 | + |
| 142 | +class FlaskContextAdapter(FrameworkAdapter): |
| 143 | + def _matches(self, app) -> bool: |
| 144 | + from flask import Flask |
| 145 | + return isinstance(app, Flask) |
| 146 | + |
| 147 | + def attach(self, app, providers: list[ContextProvider]) -> None: |
| 148 | + from my_flask_middleware import FlaskContextMiddleware |
| 149 | + app.before_request(FlaskContextMiddleware(providers).handle) |
| 150 | + |
| 151 | +register(FlaskContextAdapter()) |
| 152 | +``` |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Manual usage (tests, CLI, scripts) |
| 157 | + |
| 158 | +When there is no framework to bootstrap — unit tests, CLI tools, background |
| 159 | +jobs — set the context directly for the duration of a block: |
| 160 | + |
| 161 | +```python |
| 162 | +from sap_cloud_sdk.core.runtime_context import sdk_context, RequestContext, TENANT_ID, USER_ID |
| 163 | + |
| 164 | +# Sync: |
| 165 | +with sdk_context(RequestContext({TENANT_ID: "test-tenant", USER_ID: "test-user"})): |
| 166 | + result = some_sdk_call() |
| 167 | + |
| 168 | +# Async: |
| 169 | +from sap_cloud_sdk.core.runtime_context import async_sdk_context |
| 170 | + |
| 171 | +async with async_sdk_context(RequestContext({TENANT_ID: "test-tenant"})): |
| 172 | + result = await some_async_sdk_call() |
| 173 | +``` |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## Running the tests |
| 178 | + |
| 179 | +```bash |
| 180 | +uv run pytest tests/core/unit/runtime_context/ |
| 181 | +``` |
0 commit comments