openframe-protocol-base 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openframe_protocol_base-0.3.0/.gitignore +220 -0
- openframe_protocol_base-0.3.0/PKG-INFO +65 -0
- openframe_protocol_base-0.3.0/README.md +33 -0
- openframe_protocol_base-0.3.0/openframe/protocol/base/__init__.py +44 -0
- openframe_protocol_base-0.3.0/openframe/protocol/base/context.py +96 -0
- openframe_protocol_base-0.3.0/openframe/protocol/base/errors.py +244 -0
- openframe_protocol_base-0.3.0/openframe/protocol/base/testing.py +332 -0
- openframe_protocol_base-0.3.0/pyproject.toml +57 -0
- openframe_protocol_base-0.3.0/tests/conftest.py +4 -0
- openframe_protocol_base-0.3.0/tests/test_context.py +85 -0
- openframe_protocol_base-0.3.0/tests/test_errors.py +258 -0
- openframe_protocol_base-0.3.0/tests/test_testing.py +85 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
*.lcov
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
# Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# uv
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
# poetry.lock
|
|
110
|
+
# poetry.toml
|
|
111
|
+
|
|
112
|
+
# pdm
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
114
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
115
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
116
|
+
# pdm.lock
|
|
117
|
+
# pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# pixi
|
|
122
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
123
|
+
# pixi.lock
|
|
124
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
125
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
126
|
+
.pixi/*
|
|
127
|
+
!.pixi/config.toml
|
|
128
|
+
|
|
129
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
130
|
+
__pypackages__/
|
|
131
|
+
|
|
132
|
+
# Celery stuff
|
|
133
|
+
celerybeat-schedule*
|
|
134
|
+
celerybeat.pid
|
|
135
|
+
|
|
136
|
+
# Redis
|
|
137
|
+
*.rdb
|
|
138
|
+
*.aof
|
|
139
|
+
*.pid
|
|
140
|
+
|
|
141
|
+
# RabbitMQ
|
|
142
|
+
mnesia/
|
|
143
|
+
rabbitmq/
|
|
144
|
+
rabbitmq-data/
|
|
145
|
+
|
|
146
|
+
# ActiveMQ
|
|
147
|
+
activemq-data/
|
|
148
|
+
|
|
149
|
+
# SageMath parsed files
|
|
150
|
+
*.sage.py
|
|
151
|
+
|
|
152
|
+
# Environments
|
|
153
|
+
.env
|
|
154
|
+
.envrc
|
|
155
|
+
.venv
|
|
156
|
+
env/
|
|
157
|
+
venv/
|
|
158
|
+
ENV/
|
|
159
|
+
env.bak/
|
|
160
|
+
venv.bak/
|
|
161
|
+
|
|
162
|
+
# Spyder project settings
|
|
163
|
+
.spyderproject
|
|
164
|
+
.spyproject
|
|
165
|
+
|
|
166
|
+
# Rope project settings
|
|
167
|
+
.ropeproject
|
|
168
|
+
|
|
169
|
+
# mkdocs/Zensical documentation
|
|
170
|
+
/site
|
|
171
|
+
|
|
172
|
+
# mypy
|
|
173
|
+
.mypy_cache/
|
|
174
|
+
.dmypy.json
|
|
175
|
+
dmypy.json
|
|
176
|
+
|
|
177
|
+
# Pyre type checker
|
|
178
|
+
.pyre/
|
|
179
|
+
|
|
180
|
+
# pytype static type analyzer
|
|
181
|
+
.pytype/
|
|
182
|
+
|
|
183
|
+
# Cython debug symbols
|
|
184
|
+
cython_debug/
|
|
185
|
+
|
|
186
|
+
# PyCharm
|
|
187
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
188
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
189
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
190
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
191
|
+
# .idea/
|
|
192
|
+
|
|
193
|
+
# Abstra
|
|
194
|
+
# Abstra is an AI-powered process automation framework.
|
|
195
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
196
|
+
# Learn more at https://abstra.io/docs
|
|
197
|
+
.abstra/
|
|
198
|
+
|
|
199
|
+
# Visual Studio Code
|
|
200
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
|
|
201
|
+
# can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
202
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer, you
|
|
203
|
+
# could uncomment the following to ignore the entire vscode folder
|
|
204
|
+
# .vscode/
|
|
205
|
+
# Temporary file for partial code execution
|
|
206
|
+
tempCodeRunnerFile.py
|
|
207
|
+
|
|
208
|
+
# Ruff stuff:
|
|
209
|
+
.ruff_cache/
|
|
210
|
+
|
|
211
|
+
# PyPI configuration file
|
|
212
|
+
.pypirc
|
|
213
|
+
|
|
214
|
+
# Marimo
|
|
215
|
+
marimo/_static/
|
|
216
|
+
marimo/_lsp/
|
|
217
|
+
__marimo__/
|
|
218
|
+
|
|
219
|
+
# Streamlit
|
|
220
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: openframe-protocol-base
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: OpenFrame Microservice Suite - shared transport-neutral protocol mechanics (error mapping, RequestContext construction, inbound conformance suite).
|
|
5
|
+
Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-protocol
|
|
6
|
+
Project-URL: Documentation, https://furious-meteors.github.io/openframe-protocol/
|
|
7
|
+
Project-URL: Repository, https://github.com/Furious-Meteors/openframe-protocol
|
|
8
|
+
Project-URL: Changelog, https://github.com/Furious-Meteors/openframe-protocol/blob/production/.github/CHANGELOG.md
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/Furious-Meteors/openframe-protocol/issues
|
|
10
|
+
Author-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
11
|
+
Maintainer-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
12
|
+
License: MIT
|
|
13
|
+
Keywords: hexagonal,microservice,openframe,protocol
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: openframe-core<4,>=3.2.1
|
|
26
|
+
Requires-Dist: opentelemetry-api>=1.24
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-mock>=3.14; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# openframe-protocol-base
|
|
34
|
+
|
|
35
|
+
Shared, transport-neutral mechanics every `openframe-protocol-*` binding
|
|
36
|
+
package builds on. No transport dependencies of its own.
|
|
37
|
+
|
|
38
|
+
- `errors.py` — `categorize(exc) -> ErrorCategory` and `build_envelope(exc)`,
|
|
39
|
+
the one place every transport maps `OpenFrameError` (and anything else)
|
|
40
|
+
to a caller-safe error shape.
|
|
41
|
+
- `context.py` — `build_request_context(headers)`, building
|
|
42
|
+
`openframe.core.inbound.RequestContext` consistently across transports
|
|
43
|
+
(trace id, then a correlation header, then a generated uuid4).
|
|
44
|
+
- `testing.py` — `TransportConformanceTests` + `TransportDriver`, the
|
|
45
|
+
suite every binding proves itself against.
|
|
46
|
+
|
|
47
|
+
## Wiring example
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from openframe.protocol.base import build_envelope, build_request_context
|
|
51
|
+
|
|
52
|
+
context = build_request_context(request.headers)
|
|
53
|
+
try:
|
|
54
|
+
result = await handler.execute(command, context)
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
envelope = build_envelope(exc, correlation_id=context.correlation_id)
|
|
57
|
+
# serialise envelope in whatever shape this transport uses
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Status
|
|
61
|
+
|
|
62
|
+
Experimental — incubating the inbound conformance suite here until two
|
|
63
|
+
transports (HTTP, gRPC) both pass it for real, per the project's own
|
|
64
|
+
rule-of-three discipline for promoting shared infrastructure into
|
|
65
|
+
`openframe-core`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# openframe-protocol-base
|
|
2
|
+
|
|
3
|
+
Shared, transport-neutral mechanics every `openframe-protocol-*` binding
|
|
4
|
+
package builds on. No transport dependencies of its own.
|
|
5
|
+
|
|
6
|
+
- `errors.py` — `categorize(exc) -> ErrorCategory` and `build_envelope(exc)`,
|
|
7
|
+
the one place every transport maps `OpenFrameError` (and anything else)
|
|
8
|
+
to a caller-safe error shape.
|
|
9
|
+
- `context.py` — `build_request_context(headers)`, building
|
|
10
|
+
`openframe.core.inbound.RequestContext` consistently across transports
|
|
11
|
+
(trace id, then a correlation header, then a generated uuid4).
|
|
12
|
+
- `testing.py` — `TransportConformanceTests` + `TransportDriver`, the
|
|
13
|
+
suite every binding proves itself against.
|
|
14
|
+
|
|
15
|
+
## Wiring example
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from openframe.protocol.base import build_envelope, build_request_context
|
|
19
|
+
|
|
20
|
+
context = build_request_context(request.headers)
|
|
21
|
+
try:
|
|
22
|
+
result = await handler.execute(command, context)
|
|
23
|
+
except Exception as exc:
|
|
24
|
+
envelope = build_envelope(exc, correlation_id=context.correlation_id)
|
|
25
|
+
# serialise envelope in whatever shape this transport uses
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Status
|
|
29
|
+
|
|
30
|
+
Experimental — incubating the inbound conformance suite here until two
|
|
31
|
+
transports (HTTP, gRPC) both pass it for real, per the project's own
|
|
32
|
+
rule-of-three discipline for promoting shared infrastructure into
|
|
33
|
+
`openframe-core`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe.protocol.base
|
|
3
|
+
==========================
|
|
4
|
+
Shared, transport-neutral mechanics every `openframe-protocol-*` binding
|
|
5
|
+
builds on: error categorisation (`errors.py`), `RequestContext`
|
|
6
|
+
construction (`context.py`), and the inbound conformance suite
|
|
7
|
+
(`testing.py`). No transport dependencies — a binding package depends on
|
|
8
|
+
this one, never the reverse.
|
|
9
|
+
|
|
10
|
+
.. stability: experimental
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from openframe.protocol.base.context import build_request_context
|
|
15
|
+
from openframe.protocol.base.errors import (
|
|
16
|
+
ErrorCategory,
|
|
17
|
+
ErrorEnvelope,
|
|
18
|
+
build_envelope,
|
|
19
|
+
categorize,
|
|
20
|
+
raise_for_envelope,
|
|
21
|
+
)
|
|
22
|
+
from openframe.protocol.base.testing import (
|
|
23
|
+
UNEXPECTED_ERROR_SECRET,
|
|
24
|
+
DriverResult,
|
|
25
|
+
RemoteClientConformanceTests,
|
|
26
|
+
RemoteClientDriver,
|
|
27
|
+
TransportConformanceTests,
|
|
28
|
+
TransportDriver,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ErrorCategory",
|
|
33
|
+
"ErrorEnvelope",
|
|
34
|
+
"categorize",
|
|
35
|
+
"build_envelope",
|
|
36
|
+
"raise_for_envelope",
|
|
37
|
+
"build_request_context",
|
|
38
|
+
"DriverResult",
|
|
39
|
+
"TransportDriver",
|
|
40
|
+
"TransportConformanceTests",
|
|
41
|
+
"UNEXPECTED_ERROR_SECRET",
|
|
42
|
+
"RemoteClientDriver",
|
|
43
|
+
"RemoteClientConformanceTests",
|
|
44
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/protocol/base/context.py
|
|
3
|
+
=====================================
|
|
4
|
+
Builds `openframe.core.inbound.RequestContext` for an inbound transport
|
|
5
|
+
binding — the one place that decides what `correlation_id` actually is,
|
|
6
|
+
so every transport does it the same way instead of each reinventing it.
|
|
7
|
+
|
|
8
|
+
`RequestContext` itself is a frozen dataclass in core with no factory
|
|
9
|
+
function (confirmed empty — nothing in core constructs one outside its
|
|
10
|
+
own tests). `principal`/`tenant` stay `None` here; wiring real auth
|
|
11
|
+
through is deferred to `openframe-infra`/security, per draft 8 section 7's
|
|
12
|
+
"reserve the hook, don't implement it yet" rule — `context_enricher`
|
|
13
|
+
below is that hook.
|
|
14
|
+
|
|
15
|
+
Dependency order:
|
|
16
|
+
protocol/base/context → openframe.core.inbound, opentelemetry (trace API)
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import uuid
|
|
21
|
+
from typing import Callable, Mapping
|
|
22
|
+
|
|
23
|
+
from opentelemetry import propagate, trace
|
|
24
|
+
|
|
25
|
+
from openframe.core.inbound import RequestContext
|
|
26
|
+
|
|
27
|
+
__all__ = ["build_request_context"]
|
|
28
|
+
|
|
29
|
+
ContextEnricher = Callable[[RequestContext], RequestContext]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_request_context(
|
|
33
|
+
headers: Mapping[str, str],
|
|
34
|
+
*,
|
|
35
|
+
correlation_header: str = "x-correlation-id",
|
|
36
|
+
context_enricher: ContextEnricher | None = None,
|
|
37
|
+
) -> RequestContext:
|
|
38
|
+
"""
|
|
39
|
+
Build a `RequestContext` for one inbound request.
|
|
40
|
+
|
|
41
|
+
`correlation_id` resolution order (first match wins):
|
|
42
|
+
|
|
43
|
+
1. The trace id from an incoming W3C `traceparent` header, extracted
|
|
44
|
+
directly from *this request's own headers* — not from whatever
|
|
45
|
+
OTel span happens to be ambiently active. This distinction is
|
|
46
|
+
the fix for a real bug found while building this: `TelemetryMiddleware`
|
|
47
|
+
starts a span for *every* request regardless of whether one
|
|
48
|
+
actually propagated in from a caller, so checking
|
|
49
|
+
`trace.get_current_span()` (ambient state) can never tell "a
|
|
50
|
+
trace genuinely arrived" apart from "a fresh local span with no
|
|
51
|
+
upstream caller" — it would always win, making step 2 below
|
|
52
|
+
unreachable in practice. Extracting straight from `headers`
|
|
53
|
+
avoids that: no incoming `traceparent` means no trace id here,
|
|
54
|
+
full stop, regardless of what span is locally active.
|
|
55
|
+
2. The `correlation_header` request header (default
|
|
56
|
+
`X-Correlation-ID`), for a caller that sends its own id without
|
|
57
|
+
a trace context.
|
|
58
|
+
3. A freshly generated `uuid4`, so `correlation_id` is never empty.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
headers: The inbound request's headers. Lookup is
|
|
62
|
+
case-insensitive — pass whatever mapping the
|
|
63
|
+
transport already has; most ASGI frameworks'
|
|
64
|
+
header mappings already are.
|
|
65
|
+
correlation_header: Header name checked in step 2. Override only
|
|
66
|
+
if a deployment uses a non-default header.
|
|
67
|
+
context_enricher: Optional hook called with the built context
|
|
68
|
+
before it's returned, so a caller can attach
|
|
69
|
+
`principal`/`tenant` once real auth exists.
|
|
70
|
+
`None` (the default) leaves both unset — this
|
|
71
|
+
is the reserved-but-unimplemented auth seam
|
|
72
|
+
draft 8 asks for, not a finished feature.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
A `RequestContext` with a non-empty `correlation_id`.
|
|
76
|
+
"""
|
|
77
|
+
extracted = propagate.extract(headers)
|
|
78
|
+
span_context = trace.get_current_span(extracted).get_span_context()
|
|
79
|
+
if span_context.is_valid:
|
|
80
|
+
correlation_id = f"{span_context.trace_id:032x}"
|
|
81
|
+
else:
|
|
82
|
+
correlation_id = _get_header(headers, correlation_header) or str(uuid.uuid4())
|
|
83
|
+
|
|
84
|
+
context = RequestContext(correlation_id=correlation_id)
|
|
85
|
+
if context_enricher is not None:
|
|
86
|
+
context = context_enricher(context)
|
|
87
|
+
return context
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_header(headers: Mapping[str, str], name: str) -> str | None:
|
|
91
|
+
"""Case-insensitive header lookup — ASGI header mappings vary on casing."""
|
|
92
|
+
lowered = name.lower()
|
|
93
|
+
for key, value in headers.items():
|
|
94
|
+
if key.lower() == lowered:
|
|
95
|
+
return value
|
|
96
|
+
return None
|