Skip to content

Commit 79aa8ff

Browse files
authored
Middleware starlettify (#8)
drop with_plugins from middleware
1 parent e222f73 commit 79aa8ff

File tree

14 files changed

+135
-105
lines changed

14 files changed

+135
-105
lines changed

.pre-commit-config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v2.5.0
4+
hooks:
5+
- id: check-merge-conflict
6+
- id: debug-statements
7+
- id: no-commit-to-branch
8+
9+
- repo: https://github.com/ambv/black
10+
rev: 19.10b0
11+
hooks:
12+
- id: black

README.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@
33
[![PyPI version](https://badge.fury.io/py/starlette-context.svg)](https://badge.fury.io/py/starlette-context)
44
[![PyPI license](https://img.shields.io/pypi/l/ansicolortags.svg)](https://pypi.python.org/pypi/ansicolortags/)
55
[![codecov](https://codecov.io/gh/tomwojcik/starlette-context/branch/master/graph/badge.svg)](https://codecov.io/gh/tomwojcik/starlette-context)
6+
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
67

78
# starlette context
89
Middleware for Starlette that allows you to store and access the context data of a request. Can be used with logging so logs automatically use request headers such as x-request-id or x-correlation-id.
910

10-
### Motivation
11-
12-
I use FastAPI. I needed something that will allow me to log with context data. Right now I can just `log.info('Message')` and I have log (in ELK) with request id and correlation id. I don't even think about passing this data to logger. It's there automatically.
13-
1411
### Installation
1512

1613
`$ pip install starlette-context`
@@ -30,33 +27,39 @@ https://github.com/MagicStack/contextvars
3027
All other dependencies from `requirements-dev.txt` are only needed to run tests or examples. Test/dev env is dockerized if you want to try them yourself.
3128

3229
### Example
33-
**examples/simple_examples/set_context_in_middleware.py**
3430

3531
```python
3632
from starlette.applications import Starlette
33+
from starlette.middleware import Middleware
3734
from starlette.requests import Request
3835
from starlette.responses import JSONResponse
3936

4037
import uvicorn
4138
from starlette_context import context, plugins
4239
from starlette_context.middleware import ContextMiddleware
4340

41+
middleware = [
42+
Middleware(
43+
ContextMiddleware,
44+
plugins=(
45+
plugins.RequestIdPlugin(),
46+
plugins.CorrelationIdPlugin()
47+
)
48+
)
49+
]
4450

45-
app = Starlette(debug=True)
46-
app.add_middleware(ContextMiddleware.with_plugins( # easily extensible
47-
plugins.RequestIdPlugin, # request id
48-
plugins.CorrelationIdPlugin # correlation id
49-
))
51+
app = Starlette(middleware=middleware)
5052

5153

52-
@app.route('/')
54+
@app.route("/")
5355
async def index(request: Request):
5456
return JSONResponse(context.data)
5557

5658

5759
uvicorn.run(app, host="0.0.0.0")
60+
5861
```
59-
In this example the response containes a json with
62+
In this example the response contains a json with
6063
```json
6164
{
6265
"X-Correlation-ID":"5ca2f0b43115461bad07ccae5976a990",

examples/example_with_exception_handling/app.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from starlette.applications import Starlette
44
from starlette.exceptions import HTTPException
5+
from starlette.middleware import Middleware
56
from starlette.middleware.base import (
67
BaseHTTPMiddleware,
78
RequestResponseEndpoint,
@@ -14,15 +15,6 @@
1415
from examples.example_with_exception_handling.logger import log
1516
from starlette_context import middleware, plugins
1617

17-
app = Starlette(debug=True)
18-
19-
20-
@app.route("/")
21-
async def index(request: Request):
22-
log.info("pre exception")
23-
_ = 1 / 0
24-
return JSONResponse({"wont reach this place": None})
25-
2618

2719
class ExceptionHandlingMiddleware(BaseHTTPMiddleware):
2820
@staticmethod
@@ -54,15 +46,28 @@ async def dispatch(
5446

5547

5648
# middleware order is important! exc handler has to be topmost
49+
middleware = [
50+
Middleware(
51+
middleware.ContextMiddleware,
52+
plugins=(
53+
plugins.CorrelationIdPlugin(),
54+
plugins.RequestIdPlugin(),
55+
plugins.DateHeaderPlugin(),
56+
plugins.ForwardedForPlugin(),
57+
plugins.UserAgentPlugin(),
58+
),
59+
),
60+
Middleware(ExceptionHandlingMiddleware),
61+
]
62+
63+
app = Starlette(debug=True, middleware=middleware)
64+
65+
66+
@app.route("/")
67+
async def index(request: Request):
68+
log.info("pre exception")
69+
_ = 1 / 0
70+
return JSONResponse({"wont reach this place": None})
71+
5772

58-
app.add_middleware(ExceptionHandlingMiddleware)
59-
app.add_middleware(
60-
middleware.ContextMiddleware.with_plugins(
61-
plugins.UserAgentPlugin,
62-
plugins.ForwardedForPlugin,
63-
plugins.DateHeaderPlugin,
64-
plugins.RequestIdPlugin,
65-
plugins.CorrelationIdPlugin,
66-
)
67-
)
6873
uvicorn.run(app, host="0.0.0.0")

examples/example_with_logger/app.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
11
from starlette.applications import Starlette
2+
from starlette.middleware import Middleware
23
from starlette.requests import Request
34
from starlette.responses import JSONResponse
45

56
import uvicorn
67
from examples.example_with_logger.logger import log
78
from starlette_context import context, middleware, plugins
89

9-
app = Starlette(debug=True)
10+
middleware = [
11+
Middleware(
12+
middleware.ContextMiddleware,
13+
plugins=(
14+
plugins.CorrelationIdPlugin(),
15+
plugins.RequestIdPlugin(),
16+
plugins.DateHeaderPlugin(),
17+
plugins.ForwardedForPlugin(),
18+
plugins.UserAgentPlugin(),
19+
),
20+
)
21+
]
22+
23+
app = Starlette(debug=True, middleware=middleware)
1024

1125

1226
@app.route("/")
13-
async def index(request: Request):
27+
async def index(_: Request):
1428
log.info("Log from view")
1529
return JSONResponse(context.data)
1630

1731

18-
app.add_middleware(
19-
middleware.ContextMiddleware.with_plugins(
20-
plugins.CorrelationIdPlugin,
21-
plugins.RequestIdPlugin,
22-
plugins.DateHeaderPlugin,
23-
plugins.ForwardedForPlugin,
24-
plugins.UserAgentPlugin,
25-
)
26-
)
2732
uvicorn.run(app, host="0.0.0.0")

examples/simple_examples/set_context_in_middleware.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
from starlette.applications import Starlette
2+
from starlette.middleware import Middleware
23
from starlette.requests import Request
34
from starlette.responses import JSONResponse
45

56
import uvicorn
67
from starlette_context import context, plugins
78
from starlette_context.middleware import ContextMiddleware
89

9-
app = Starlette(debug=True)
10-
app.add_middleware(
11-
ContextMiddleware.with_plugins(
12-
plugins.RequestIdPlugin, plugins.CorrelationIdPlugin
10+
middleware = [
11+
Middleware(
12+
ContextMiddleware,
13+
plugins=(plugins.RequestIdPlugin(), plugins.CorrelationIdPlugin()),
1314
)
14-
)
15+
]
16+
17+
app = Starlette(debug=True, middleware=middleware)
1518

1619

1720
@app.route("/")

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ autoflake
2020
mypy
2121
black
2222
isort
23+
pre-commit-hooks

setup.py

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

33
import setuptools
44

5-
VERSION = "0.2.0"
5+
VERSION = "0.2.1"
66

77

88
def get_long_description():

starlette_context/ctx.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ def data(self) -> dict:
2323
"""
2424
Dump this to json. Object itself it not serializable.
2525
"""
26-
return _request_scope_context_storage.get()
26+
try:
27+
return _request_scope_context_storage.get()
28+
except LookupError as e:
29+
raise RuntimeError(
30+
"You didn't use ContextMiddleware or "
31+
"you're trying to access `context` object "
32+
"outside of the request-response cycle."
33+
) from e
2734

2835
def copy(self) -> dict:
2936
"""

starlette_context/middleware.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from contextvars import Token
2-
from typing import List, Type, Union
2+
from typing import Optional, Sequence
33

44
from starlette.middleware.base import (
55
BaseHTTPMiddleware,
@@ -18,20 +18,13 @@ class ContextMiddleware(BaseHTTPMiddleware):
1818
If not used, you won't be able to use context object.
1919
"""
2020

21-
plugins: List[Plugin] = []
22-
23-
@classmethod
24-
def with_plugins(
25-
cls, *plugins: Union[Plugin, Type[Plugin]]
26-
) -> Type["ContextMiddleware"]:
27-
for plugin in plugins:
28-
if isinstance(plugin, Plugin):
29-
cls.plugins.append(plugin)
30-
elif issubclass(plugin, Plugin):
31-
cls.plugins.append(plugin())
32-
else:
33-
raise TypeError("Only plugins are allowed.")
34-
return cls
21+
def __init__(
22+
self, plugins: Optional[Sequence[Plugin]] = None, *args, **kwargs
23+
) -> None:
24+
super().__init__(*args, **kwargs)
25+
self.plugins = plugins or ()
26+
if not all([isinstance(plugin, Plugin) for plugin in self.plugins]):
27+
raise TypeError("This is not a valid instance of a plugin")
3528

3629
async def set_context(self, request: Request) -> dict:
3730
"""

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def headers():
3232

3333
@pytest.fixture(scope="function", autouse=True)
3434
def mocked_middleware() -> middleware.ContextMiddleware:
35-
return middleware.ContextMiddleware(MagicMock())
35+
return middleware.ContextMiddleware(app=MagicMock())
3636

3737

3838
@pytest.fixture(scope="function", autouse=True)

tests/test_integration/test_async_body.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from starlette.applications import Starlette
2+
from starlette.middleware import Middleware
23
from starlette.requests import Request
34
from starlette.responses import JSONResponse
45
from starlette.testclient import TestClient
56

7+
from starlette_context import context, plugins
68
from starlette_context.middleware import ContextMiddleware
79

8-
from starlette_context import plugins, context
9-
1010

1111
class GetPayloadUsingPlugin(plugins.Plugin):
1212
key = "from_plugin"
@@ -24,10 +24,12 @@ async def set_context(self, request: Request) -> dict:
2424
return {"from_middleware": await request.json(), **from_plugin}
2525

2626

27-
app = Starlette()
28-
app.add_middleware(
29-
GetPayloadFromBodyMiddleware.with_plugins(GetPayloadUsingPlugin)
30-
)
27+
middleware = [
28+
Middleware(
29+
GetPayloadFromBodyMiddleware, plugins=(GetPayloadUsingPlugin(),)
30+
)
31+
]
32+
app = Starlette(middleware=middleware)
3133

3234

3335
@app.route("/", methods=["POST"])
@@ -46,6 +48,3 @@ def test_async_body():
4648
"from_plugin": {"test": "payload"},
4749
}
4850
assert expected_resp == resp.json()
49-
50-
# ugly cleanup
51-
ContextMiddleware.plugins = []

tests/test_integration/test_context_no_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ async def index(request: Request):
1616

1717

1818
def test_no_middleware():
19-
with pytest.raises(LookupError):
19+
with pytest.raises(RuntimeError):
2020
client.get("/")

0 commit comments

Comments
 (0)