Skip to content

Commit 6179334

Browse files
Docs cleanup
1 parent 76453b4 commit 6179334

7 files changed

Lines changed: 225 additions & 53 deletions

File tree

src/sap_cloud_sdk/core/bootstrap.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,19 @@
99
def bootstrap(app: Any, providers: Optional[List[ContextProvider]] = None) -> None:
1010
"""Wire the SDK runtime context into your application framework.
1111
12-
Call this once at application startup. The SDK will attach a middleware
13-
to *app* that populates :class:`~sap_cloud_sdk.core.runtime_context.RequestContext`
14-
on every inbound request by running all *providers* against it and merging
15-
the results.
12+
Call once at startup. On every inbound request the SDK will run all
13+
*providers* against the request, merge the results, and make them
14+
available via :func:`~sap_cloud_sdk.core.runtime_context.get_context`.
1615
17-
After bootstrapping, any SDK module (auditlog, telemetry, etc.) can call
18-
:func:`~sap_cloud_sdk.core.runtime_context.get_context` to read
19-
tenant/user information without knowing about headers or auth providers.
20-
21-
The framework is detected automatically from the *app* type. Supported
22-
frameworks are determined by registered
23-
:class:`~sap_cloud_sdk.core.runtime_context.FrameworkAdapter` instances —
24-
new frameworks are added by registering an adapter, not by editing this file.
16+
The framework is detected automatically from the *app* type via the
17+
registered :class:`~sap_cloud_sdk.core.runtime_context.FrameworkAdapter`
18+
instances — adding support for a new framework never requires editing
19+
this function.
2520
2621
Args:
2722
app: The application instance to attach the middleware to.
28-
providers: One or more :class:`~sap_cloud_sdk.core.runtime_context.ContextProvider`
29-
instances. Defaults to ``[IASContextProvider()]``.
23+
providers: Context providers to run on each request. Defaults to
24+
``[IASContextProvider(), HeaderContextProvider()]``.
3025
3126
Raises:
3227
TypeError: If no registered adapter recognises *app*.
@@ -35,10 +30,10 @@ def bootstrap(app: Any, providers: Optional[List[ContextProvider]] = None) -> No
3530
3631
from sap_cloud_sdk import bootstrap
3732
38-
bootstrap(app) # IASContextProvider by default
33+
bootstrap(app) # IAS + SAP headers by default
3934
40-
# multiple providers:
41-
bootstrap(app, providers=[IASContextProvider(), MyCustomProvider()])
35+
# custom providers:
36+
bootstrap(app, providers=[IASContextProvider(), MyProvider()])
4237
"""
4338
if not providers:
4439
providers = [IASContextProvider(), HeaderContextProvider()]

src/sap_cloud_sdk/core/runtime_context/__init__.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
1-
"""SDK-wide per-request runtime context.
1+
"""SDK-wide runtime context for the current execution.
22
3-
Provides a provider-agnostic way for SDK modules to access caller-identity
4-
fields (tenant, user, trigger type) without coupling to a specific auth
5-
provider or HTTP framework.
3+
Lets SDK modules read caller-identity information (tenant, user, trigger type)
4+
without knowing about the invocation source — HTTP, gRPC, message queue, etc.
65
7-
Typical usage — wire once at app startup::
6+
Wire once at startup::
87
98
from sap_cloud_sdk import bootstrap
10-
from sap_cloud_sdk.core.runtime_context import IASContextProvider
119
12-
bootstrap(app, providers=[IASContextProvider()])
10+
bootstrap(app) # defaults to IASContextProvider + HeaderContextProvider
1311
14-
Then read anywhere in the SDK or application code::
12+
Then read anywhere::
1513
16-
from sap_cloud_sdk.core.runtime_context import get_context
14+
from sap_cloud_sdk.core.runtime_context import get_context, TENANT_ID, USER_ID
1715
1816
ctx = get_context()
19-
print(ctx.tenant_id) # e.g. "abc-123"
20-
print(ctx.user_id) # e.g. "user-uuid"
17+
ctx.get(TENANT_ID) # -> "abc-123" or None
18+
ctx.get(USER_ID) # -> "user-uuid" or None
2119
22-
For tests or CLI usage without HTTP::
20+
In tests or scripts without a framework::
2321
24-
from sap_cloud_sdk.core.runtime_context import sdk_context, RequestContext
22+
from sap_cloud_sdk.core.runtime_context import sdk_context, RequestContext, TENANT_ID
2523
26-
with sdk_context(RequestContext(tenant_id="test-tenant")):
27-
... # get_context() returns that context here
24+
with sdk_context(RequestContext({TENANT_ID: "test-tenant"})):
25+
...
2826
"""
2927

3028
from sap_cloud_sdk.core.runtime_context._context import (

src/sap_cloud_sdk/core/runtime_context/_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
class RequestContext:
13-
"""Immutable typed bag of per-request values set by context providers.
13+
"""Immutable typed bag of caller-identity values for the current execution.
1414
1515
Values are keyed by :class:`ContextKey` instances, which carry the
1616
expected type. Use :meth:`get` to read a value and :meth:`with_value`

src/sap_cloud_sdk/core/runtime_context/_envelope.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66

77
@dataclass
88
class RequestEnvelope:
9-
"""Normalized view of an inbound request, independent of framework.
9+
"""Framework-agnostic view of an inbound request passed to providers.
1010
11-
Framework middlewares build this from their native request object.
12-
:class:`~sap_cloud_sdk.core.runtime_context.ContextProvider` implementations
13-
read from it — they never touch framework-specific types.
11+
The framework middleware populates this; providers read from it. This
12+
means providers work identically regardless of whether the request came
13+
from Starlette, Flask, gRPC, or a test.
1414
1515
Attributes:
16-
headers: Case-insensitive HTTP headers (or equivalent for gRPC/etc.).
17-
body: Raw request body bytes. ``None`` if not extracted.
18-
metadata: Catch-all for framework extras (query params, gRPC metadata,
19-
connection info, etc.). Reserved for future providers.
16+
headers: Request headers as a plain dict (lowercased keys recommended).
17+
body: Raw request body. ``None`` if not needed by any provider.
18+
metadata: Framework-specific extras query params, gRPC metadata, etc.
19+
Currently unused; reserved for future providers.
2020
"""
2121

2222
headers: Dict[str, str] = field(default_factory=dict)

src/sap_cloud_sdk/core/runtime_context/_protocol.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,19 @@
88

99
@runtime_checkable
1010
class ContextProvider(Protocol):
11-
"""Extract a :class:`RequestContext` from a :class:`RequestEnvelope`.
11+
"""Extracts a :class:`RequestContext` from a :class:`RequestEnvelope`.
1212
13-
Implement this protocol to teach the SDK how to read caller-identity
14-
information from a specific auth provider (IAS, XSUAA, etc.).
15-
16-
The envelope is framework-agnostic — providers never touch Starlette,
17-
Flask, or gRPC types directly. The framework middleware is responsible
18-
for building the envelope.
13+
Implement this to add a new auth provider or header convention. The envelope
14+
is framework-agnostic — providers never touch Starlette, Flask, or gRPC types.
1915
2016
Example::
2117
18+
MY_KEY = ContextKey[str]("my_key")
19+
2220
class MyProvider(ContextProvider):
2321
def extract(self, envelope: RequestEnvelope) -> RequestContext:
24-
token = envelope.headers.get("x-my-token", "")
25-
return RequestContext(tenant_id=decode(token).tenant)
22+
value = envelope.headers.get("x-my-header", "")
23+
return RequestContext({MY_KEY: value} if value else {})
2624
"""
2725

2826
def extract(self, envelope: RequestEnvelope) -> RequestContext: # pragma: no cover

src/sap_cloud_sdk/core/runtime_context/_registry.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ def get_registry() -> List["FrameworkAdapter"]:
2121

2222

2323
class FrameworkAdapter(ABC):
24-
"""Base class for framework-specific context middleware adapters.
24+
"""Connects a framework or invocation source to the SDK runtime context.
2525
26-
Subclasses know how to detect a framework's app object and attach the
27-
appropriate context middleware to it. Register at module level so that
26+
Subclasses know how to detect a specific app type and attach the SDK's
27+
context pipeline to it. Register at module level so that
2828
:func:`~sap_cloud_sdk.core.bootstrap.bootstrap` can discover them without
2929
importing framework-specific code directly.
3030
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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

Comments
 (0)