Skip to content

Commit

Permalink
Finish up composition docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche committed Nov 7, 2023
1 parent 1fd45af commit c2aa5c1
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 17 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ An _uapi_ app can be easily integrated into an existing project based on one of
Using _uapi_ enables you to:

- write **either async or sync** styles of handlers, depending on the underlying framework used.
- use and customize a **depedency injection** system, based on [incant](https://github.com/Tinche/incant/).
- automatically **serialize and deserialize** data through [attrs](https://www.attrs.org/en/stable/) and [cattrs](https://cattrs.readthedocs.io/en/latest/).
- use and customize a **function composition** (dependency injection) system, based on [incant](https://incant.threeofwands.com).
- automatically **serialize and deserialize** data through [attrs](https://www.attrs.org) and [cattrs](https://catt.rs).
- generate and use **OpenAPI** descriptions of your endpoints.
- optionally **type-check** your handlers with [Mypy](https://mypy.readthedocs.io/en/stable/).
- write and use reusable and **powerful middleware**, which integrates with the OpenAPI schema.
- **integrate** with existing apps based on [Django](https://docs.djangoproject.com/en/stable/), [Starlette](https://www.starlette.io/), [Flask](https://flask.palletsprojects.com/en/latest/), [Quart](https://pgjones.gitlab.io/quart/) or [aiohttp](https://docs.aiohttp.org/en/stable/).
- **integrate** with existing apps based on [Django](https://docs.djangoproject.com/en/stable/), [Starlette](https://www.starlette.io/), [Flask](https://flask.palletsprojects.com), [Quart](https://pgjones.gitlab.io/quart/) or [aiohttp](https://docs.aiohttp.org).

Here's a simple taste:

Expand Down
101 changes: 101 additions & 0 deletions docs/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,104 @@ This is true of all dependency hooks and middleware.
The final handler signature available to _uapi_ at time of serving contains all the dependencies as function arguments.
```

## Extending the Context

The composition context can be extended with arbitrary dependencies.

For example, imagine your application needs to perform HTTP requests.
Ideally, the handlers should use a shared connection pool instance for efficiency.
Here's a complete implementation of a very simple HTTP proxy.
The example can be pasted and ran as-is as long as Starlette and Uvicorn are available.

```python
from asyncio import run

from httpx import AsyncClient

from uapi.starlette import App

app = App()

_client = AsyncClient() # We only want one.
app.incant.register_by_type(lambda: _client, type=AsyncClient)


@app.get("/proxy")
async def proxy(client: AsyncClient) -> str:
"""We just return the payload at www.example.com."""
return (await client.get("http://example.com")).read().decode()


run(app.run())
```

## Integrating the `svcs` Package

If you'd like to get more serious about application architecture, one of the approaches is to use the [svcs](https://svcs.hynek.me/) library.
Here's a way of integrating it into _uapi_.

```python
from httpx import AsyncClient
from svcs import Container, Registry

from uapi.starlette import App

reg = Registry()
reg.register_value(AsyncClient, AsyncClient(), enter=False)

app = App()
app.incant.register_by_type(
lambda: Container(reg), type=Container, is_ctx_manager="async"
)


@app.get("/proxy")
async def proxy(container: Container) -> str:
"""We just return the payload at www.example.com."""
client = await container.aget(AsyncClient)
return (await client.get("http://example.com")).read().decode()
```

We can go even further and instead of providing the `container`, we can provide anything the container contains too.

```python
from collections.abc import Callable
from inspect import Parameter

from httpx import AsyncClient
from svcs import Container, Registry

from uapi.starlette import App

reg = Registry()
reg.register_value(AsyncClient, AsyncClient(), enter=False)

app = App()
app.incant.register_by_type(
lambda: Container(reg), type=Container, is_ctx_manager="async"
)


def svcs_hook_factory(parameter: Parameter) -> Callable:
t = parameter.annotation

async def from_container(c: Container):
return await c.aget(t)

return from_container


app.incant.register_hook_factory(lambda p: p.annotation in reg, svcs_hook_factory)


@app.get("/proxy")
async def proxy(client: AsyncClient) -> str:
"""We just return the payload at www.example.com."""
return (await client.get("http://example.com")).read().decode()
```

```{note}
The _svcs_ library includes integrations for several popular web frameworks, and code examples for them.
The examples shown here are independent of the underlying web framework used; they will work on all of them (with a potential sync/async tweak).
```
14 changes: 7 additions & 7 deletions src/uapi/quart.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ async def run(

if handle_signals:
server = Server(config=config)

await server.serve()
else:

class NoSignalsServer(Server):
Expand All @@ -221,13 +221,13 @@ def install_signal_handlers(self) -> None:

server = NoSignalsServer(config=config)

t = create_task(server.serve())
t = create_task(server.serve())

with suppress(BaseException):
while True:
await sleep(360)
server.should_exit = True
await t
with suppress(BaseException):
while True:
await sleep(360)
server.should_exit = True
await t


App: TypeAlias = QuartApp
Expand Down
14 changes: 7 additions & 7 deletions src/uapi/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ async def run(

if handle_signals:
server = Server(config=config)

await server.serve()
else:

class NoSignalsServer(Server):
Expand All @@ -245,13 +245,13 @@ def install_signal_handlers(self) -> None:

server = NoSignalsServer(config=config)

t = create_task(server.serve())
t = create_task(server.serve())

with suppress(BaseException):
while True:
await sleep(360)
server.should_exit = True
await t
with suppress(BaseException):
while True:
await sleep(360)
server.should_exit = True
await t


App: TypeAlias = StarletteApp
Expand Down

0 comments on commit c2aa5c1

Please sign in to comment.