Skip to content

Commit

Permalink
Added support for the Litestar framework (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed Oct 17, 2023
1 parent 87b4e07 commit 5e3945e
Show file tree
Hide file tree
Showing 12 changed files with 446 additions and 4 deletions.
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The following web frameworks are catered for:
* AIOHTTP_
* Django_
* FastAPI_
* Litestar_
* Starlette_

WSGI is not supported because Asphalt focuses on asynchronous operation and WSGI is a
Expand All @@ -26,6 +27,7 @@ synchronous web application gateway standard.
.. _AIOHTTP: https://docs.aiohttp.org/
.. _Django: https://www.djangoproject.com/
.. _FastAPI: https://fastapi.tiangolo.com/
.. _Litestar: https://litestar.dev/
.. _Starlette: https://www.starlette.io/

Project links
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"asphalt": ("https://asphalt.readthedocs.io/en/latest/", None),
"litestar": ("https://docs.litestar.dev/latest/", None),
}
63 changes: 63 additions & 0 deletions docs/integrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,67 @@ Resources available to HTTP request handlers:

Resources available to websocket handlers:

* the ASGI scope of the request

* type: `asgiref.typing.WebSocketScope`_
* name: ``default``

Litestar
--------

Component: ``litestar`` (:class:`~.litestar.LitestarComponent`)

Example: :github:`examples/litestar`

This integration is based on the ASGI 3.0 integration.

Litestar has its own dependency injection system which can optionally be used to inject
Asphalt resources in web endpoints. This can be done by using
:class:`~asphalt.web.litestar.AsphaltProvide` instead of :class:`~litestar.di.Provide`::

from litestar import get
from asphalt.web.litestar import AsphaltProvide

@get("/endpointname", dependencies={"myresource": AsphaltProvide(SomeConnection)})
async def myendpoint(myresource: SomeConnection) -> None:
...

This would be roughly equivalent to::

from litestar import get
from asphalt.core import require_resource

@get("/endpointname")
async def myendpoint() -> None:
myresource = require_resource(SomeConnection)
...

Resources available on the global context:

* the application object

* type: `asgiref.typing.ASGI3Application`_ or `litestar.Litestar`_
* name: ``default``

Resources available to HTTP request handlers:

* the ASGI scope of the request

* type: `asgiref.typing.HTTPScope`_
* name: ``default``

* the request object

* type: `litestar.Request`_
* name: ``default``

.. note::
The request resource is created from the ASGI scope object by the Asphalt
middleware, and does **NOT** share state with any request object provided by the
Litestar framework

Resources available to websocket handlers:

* the ASGI scope of the request

* type: `asgiref.typing.WebSocketScope`_
Expand Down Expand Up @@ -166,5 +227,7 @@ Resources available to request handlers:
.. _starlette.requests.Request: https://www.starlette.io/requests/
.. _starlette.applications.Starlette: https://www.starlette.io/applications/
.. _fastapi.FastAPI: https://fastapi.tiangolo.com/tutorial/first-steps/
.. _litestar.Request: https://docs.litestar.dev/latest/usage/requests.html
.. _litestar.Litestar: https://docs.litestar.dev/latest/usage/applications.html
.. _aiohttp.web_app.Application: https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Application
.. _aiohttp.web_request.Request: https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Request
5 changes: 5 additions & 0 deletions docs/modules/litestar.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:mod:`asphalt.web.litestar`
===========================

.. automodule:: asphalt.web.litestar
:members:
5 changes: 5 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ Version history

This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.

**UNRELEASED**

- Dropped Python 3.7 support
- Added support for the Litestar framework

**1.2.1**

- Fixed unintentional change where the asgiref dependency was dropped from the
Expand Down
15 changes: 15 additions & 0 deletions examples/litestar/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.. highlight:: bash

To install the prerequisites for this example::

pip install asphalt-web[litestar]

To start the ``static`` example::

PYTHONPATH=. asphalt run config.yaml --service static

To start the ``dynamic`` example::

PYTHONPATH=. asphalt run config.yaml --service dynamic

Then, navigate to http://localhost:8000 in your browser.
12 changes: 12 additions & 0 deletions examples/litestar/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
static:
component:
type: litestar
route_handlers: ["static:root"]

dynamic:
component:
type: litestar
components:
myroot:
type: dynamic:WebRootComponent
12 changes: 12 additions & 0 deletions examples/litestar/dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from asphalt.core import Component, Context, require_resource
from litestar import Litestar, get


@get("/")
async def root() -> str:
return "Hello, world!"


class WebRootComponent(Component):
async def start(self, ctx: Context) -> None:
require_resource(Litestar).register(root)
6 changes: 6 additions & 0 deletions examples/litestar/static.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from litestar import get


@get("/")
async def root() -> str:
return "Hello, world!"
14 changes: 10 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ fastapi = [
"fastapi >= 0.75",
"uvicorn >= 0.17.6",
]
litestar = [
"asgiref ~= 3.5",
"litestar >= 2.2",
"uvicorn >= 0.17.6",
]
starlette = [
"asgiref ~= 3.5",
"starlette >= 0.17",
Expand All @@ -69,21 +74,23 @@ test = [
"websockets",
"aiohttp >= 3.8; python_version < '3.12'",
"Django >= 3.2; python_implementation == 'CPython'",
"asphalt-web[asgi3,starlette,fastapi]",
"asphalt-web[asgi3,fastapi,starlette]",
"litestar >= 2.2; python_implementation == 'CPython'",
]
doc = [
"Sphinx >= 7.0",
"sphinx_rtd_theme >= 1.3.0",
"sphinx-autodoc-typehints >= 1.22",
"sphinx-tabs >= 3.3.1",
"asphalt-web[aiohttp,asgi3,starlette,django,fastapi]",
"asphalt-web[aiohttp,asgi3,django,fastapi,litestar,starlette]",
]

[project.entry-points."asphalt.components"]
aiohttp = "asphalt.web.aiohttp:AIOHTTPComponent"
asgi3 = "asphalt.web.asgi3:ASGIComponent"
django = "asphalt.web.django:DjangoComponent"
fastapi = "asphalt.web.fastapi:FastAPIComponent"
litestar = "asphalt.web.litestar:LitestarComponent"
starlette = "asphalt.web.starlette:StarletteComponent"

[tool.setuptools_scm]
Expand All @@ -103,7 +110,6 @@ select = [
"PGH", # pygrep-hooks
"UP", # pyupgrade
]
target-version = "py38"

[tool.ruff.isort]
known-first-party = ["asphalt.web"]
Expand Down Expand Up @@ -132,5 +138,5 @@ commands = python -m pytest {posargs}
[testenv:docs]
extras = doc
commands = sphinx-build docs build/sphinx
commands = sphinx-build -n docs build/sphinx
"""
104 changes: 104 additions & 0 deletions src/asphalt/web/litestar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from __future__ import annotations

from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Any

from asgiref.typing import ASGI3Application, HTTPScope, WebSocketScope
from asphalt.core import Context, require_resource, resolve_reference
from litestar import Litestar, Request
from litestar.middleware import AbstractMiddleware
from litestar.types import ControllerRouterHandler, Receive, Scope, Send

from asphalt.web.asgi3 import ASGIComponent


@dataclass(frozen=True)
class AsphaltProvide:
"""
Asphalt's version of Litestar's :func:`~litestar.di.Provide`.
This should be marked as the default value on a parameter that should receive an
Asphalt resource.
:param cls: the type of the resource
:param name: the name of the resource within its unique type
"""

cls: type
name: str = "default"

async def __call__(self) -> Any:
return require_resource(self.cls, self.name)


class AsphaltMiddleware(AbstractMiddleware):
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
async with Context() as ctx:
if scope["type"] == "http":
ctx.add_resource(scope, types=[HTTPScope])
ctx.add_resource(Request(scope))
elif scope["type"] == "websocket":
ctx.add_resource(scope, types=[WebSocketScope])
ctx.add_resource(Request(scope))

await self.app(scope, receive, send)


class LitestarComponent(ASGIComponent[Litestar]):
"""
A component that serves a Litestar application.
:param host: the IP address to bind to
:param port: the port to bind to
:param route_handlers: list of callables or module:varname references that point
to the routers / controolers / handlers that should be attached to the
application
:param middlewares: list of callables or dicts to be added as middleware using
:meth:`add_middleware`
.. note::
The following options are preset here:
* ``debug``: set to the value of
`__debug__ <https://docs.python.org/3/library/constants.html#debug__>`_;
unless overridden
* ``logging_config``: always set to ``None``, as Asphalt handles logging
configuration
If you supply the a pre-made application object that has route handlers already
in it, know that any middleware added to it during the initialization of
:class:`LitestarComponent` will **NOT** apply to the route handlers previously
added to the application
"""

def __init__(
self,
components: dict[str, dict[str, Any] | None] | None = None,
*,
host: str = "127.0.0.1",
port: int = 8000,
route_handlers: Sequence[ControllerRouterHandler | str] = (),
middlewares: Sequence[Callable[..., ASGI3Application] | dict[str, Any]] = (),
config: dict[str, Any] | None = None,
) -> None:
config_ = config or {}
config_.setdefault("debug", __debug__)
config_["logging_config"] = None
app = Litestar(**config_)
super().__init__(
components, app=app, middlewares=middlewares, host=host, port=port
)

for item in route_handlers:
if isinstance(item, str):
handler = resolve_reference(item)
else:
handler = item

self.original_app.register(handler)

def setup_asphalt_middleware(self, app: Litestar) -> ASGI3Application:
return AsphaltMiddleware(app=app)

0 comments on commit 5e3945e

Please sign in to comment.