-
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
764 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from __future__ import annotations | ||
|
||
from starlette.requests import Request | ||
from starlette.responses import JSONResponse | ||
|
||
import svcs | ||
|
||
|
||
async def healthy(request: Request) -> JSONResponse: | ||
""" | ||
Ping all external services. | ||
""" | ||
ok: list[str] = [] | ||
failing: list[dict[str, str]] = [] | ||
code = 200 | ||
|
||
for svc in svcs.starlette.get_pings(request): | ||
try: | ||
await svc.aping() | ||
ok.append(svc.name) | ||
except Exception as e: | ||
failing.append({svc.name: repr(e)}) | ||
code = 500 | ||
|
||
return JSONResponse( | ||
content={"ok": ok, "failing": failing}, status_code=code | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
|
||
from typing import AsyncGenerator | ||
|
||
from starlette.applications import Starlette | ||
from starlette.middleware import Middleware | ||
from starlette.requests import Request | ||
from starlette.responses import JSONResponse | ||
from starlette.routing import Route | ||
|
||
import svcs | ||
|
||
|
||
config = {"db_url": os.environ.get("DB_URL", "sqlite:///:memory:")} | ||
|
||
|
||
class Database: | ||
@classmethod | ||
async def connect(cls, db_url: str) -> Database: | ||
... | ||
return Database() | ||
|
||
async def get_user(self, user_id: int) -> dict[str, str]: | ||
return {} # not interesting here | ||
|
||
|
||
async def get_user(request: Request) -> JSONResponse: | ||
db = await svcs.starlette.aget(request, Database) | ||
|
||
try: | ||
return JSONResponse( | ||
{"data": await db.get_user(request.path_params["user_id"])} | ||
) | ||
except Exception as e: | ||
return JSONResponse({"oh no": e.args[0]}) | ||
|
||
|
||
@svcs.starlette.lifespan | ||
async def lifespan( | ||
app: Starlette, registry: svcs.Registry | ||
) -> AsyncGenerator[dict[str, object], None]: | ||
async def connect_to_db() -> Database: | ||
return await Database.connect(config["db_url"]) | ||
|
||
registry.register_factory(Database, connect_to_db) | ||
|
||
yield {"your": "other stuff"} | ||
|
||
|
||
app = Starlette( | ||
lifespan=lifespan, | ||
middleware=[Middleware(svcs.starlette.SVCSMiddleware)], | ||
routes=[Route("/users/{user_id}", get_user)], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
from unittest.mock import Mock | ||
|
||
import pytest | ||
|
||
from starlette.testclient import TestClient | ||
|
||
from simple_starlette_app import Database, app, lifespan | ||
|
||
|
||
@pytest.fixture(name="client") | ||
def _client(): | ||
with TestClient(app) as client: | ||
yield client | ||
|
||
|
||
def test_db_goes_boom(client): | ||
""" | ||
Database errors are handled gracefully. | ||
""" | ||
|
||
# IMPORTANT: Overwriting must happen AFTER the app is ready! | ||
db = Mock(spec_set=Database) | ||
db.get_user.side_effect = Exception("boom") | ||
lifespan.registry.register_value(Database, db) | ||
|
||
resp = client.get("/users/42") | ||
|
||
assert {"oh no": "boom"} == resp.json() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,5 +12,6 @@ aiohttp | |
fastapi | ||
flask | ||
pyramid | ||
starlette | ||
custom | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# Starlette | ||
|
||
*svcs*'s [Starlette](https://www.starlette.io/) integration stores the {class}`svcs.Registry` on the *lifespan state* and the {class}`svcs.Container` is added to the request state using a [pure ASGI middleware](https://www.starlette.io/middleware/#pure-asgi-middleware). | ||
|
||
(starlette-init)= | ||
|
||
## Initialization | ||
|
||
To use *svcs* with Starlette, you have to pass a [*lifespan*](https://www.starlette.io/lifespan/) -- that has been wrapped by {class}`svcs.starlette.lifespan` -- and a {class}`~svcs.starlette.SVCSMiddleware` to your application: | ||
|
||
```python | ||
from starlette.applications import Starlette | ||
from starlette.middleware import Middleware | ||
|
||
import svcs | ||
|
||
|
||
@svcs.starlette.lifespan | ||
async def lifespan(app: Starlette, registry: svcs.Registry): | ||
registry.register_factory(Database, Database.connect) | ||
|
||
yield {"your": "other stuff"} | ||
|
||
# Registry is closed automatically when the app is done. | ||
|
||
|
||
app = Starlette( | ||
lifespan=lifespan, | ||
middleware=[Middleware(svcs.starlette.SVCSMiddleware)], | ||
routes=[...], | ||
) | ||
``` | ||
|
||
(starlette-get)= | ||
|
||
## Service Acquisition | ||
|
||
You can either use {func}`svcs.starlette.svcs_from`: | ||
|
||
```python | ||
from svcs.starlette import svcs_from | ||
|
||
async def view(request): | ||
db = await svcs_from(request).aget(Database) | ||
``` | ||
|
||
Or you can use {func}`svcs.starlette.aget` to extract your services directly: | ||
|
||
```python | ||
import svcs | ||
|
||
async def view(request): | ||
db = await svcs.starlette.aget(request, Database) | ||
``` | ||
|
||
(starlette-health)= | ||
|
||
## Health Checks | ||
|
||
As with services, you have the option to either {func}`svcs.starlette.svcs_from` on the request or go straight for {func}`svcs.starlette.get_pings`. | ||
|
||
A health endpoint could look like this: | ||
|
||
```{literalinclude} ../examples/starlette/health_check.py | ||
``` | ||
|
||
## Testing | ||
|
||
The centralized service registry makes it straight-forward to selectively replace dependencies within your application in tests even if you have many dependencies to handle. | ||
|
||
Let's take this simple application as an example: | ||
|
||
```{literalinclude} ../examples/starlette/simple_starlette_app.py | ||
``` | ||
|
||
Now if you want to make a request against the `get_user` view, but want the database to raise an error to see if it's properly handled, you can do this: | ||
|
||
```{literalinclude} ../examples/starlette/test_simple_starlette_app.py | ||
``` | ||
|
||
As you can see, we can inspect the decorated lifespan function to get the registry that got injected and you can overwrite it later. | ||
|
||
::: {important} | ||
You must overwrite *after* the application has been initialized. | ||
Otherwise the lifespan function overwrites your settings. | ||
::: | ||
|
||
|
||
## Cleanup | ||
|
||
If you initialize the application with a lifespan and middleware as shown above, and use {func}`~svcs.starlette.svcs_from` or {func}`~svcs.starlette.aget` to get your services, everything is cleaned up behind you automatically. | ||
|
||
|
||
## API Reference | ||
|
||
### Application Life Cycle | ||
|
||
```{eval-rst} | ||
.. module:: svcs.starlette | ||
.. autoclass:: lifespan(lifespan) | ||
.. autoclass:: SVCSMiddleware | ||
.. seealso:: :ref:`fastapi-init` | ||
``` | ||
|
||
|
||
### Service Acquisition | ||
|
||
```{eval-rst} | ||
.. function:: aget(request: starlette.requests.Request, svc_type1: type, ...) | ||
:async: | ||
Same as :meth:`svcs.Container.aget`, but uses the container from *request*. | ||
.. autofunction:: aget_abstract | ||
.. autofunction:: svcs_from | ||
.. autofunction:: get_pings | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.