Skip to content

Commit

Permalink
add sphinx docs (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
thrau authored Jul 19, 2024
1 parent f3c553d commit c6f2cff
Show file tree
Hide file tree
Showing 17 changed files with 667 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
paths-ignore:
- 'README.md'
- 'docs/**'
branches:
- main
pull_request:
Expand Down
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ test-coverage: venv
coveralls: venv
$(VENV_RUN); coveralls

$(VENV_DIR)/.docs-install: pyproject.toml $(VENV_ACTIVATE)
$(VENV_RUN); pip install -e .[docs]
touch $(VENV_DIR)/.docs-install

install-docs: $(VENV_DIR)/.docs-install

docs: install-docs
$(VENV_RUN); cd docs && make html

dist: venv
$(VENV_RUN); pip install --upgrade build; python -m build

Expand Down
20 changes: 20 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
Empty file added docs/_static/.gitkeep
Empty file.
Binary file added docs/_static/rolo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added docs/_templates/.gitkeep
Empty file.
50 changes: 50 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = 'Rolo'
copyright = '2024, LocalStack'
author = 'Thomas Rausch'
release = '0.6.x'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = [
'myst_parser'
]

templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']


# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "furo"
html_static_path = ["_static"]
html_title = "Rolo documentation"

html_logo = "_static/rolo.png"
html_theme_options = {
"top_of_page_buttons": ["view", "edit"],
"source_repository": "https://github.com/localstack/rolo/",
"source_branch": "main",
"source_directory": "docs/",
"footer_icons": [
{
"name": "GitHub",
"url": "https://github.com/localstack/rolo",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
""",
"class": "",
},
],
}
32 changes: 32 additions & 0 deletions docs/gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Gateway
=======

The `Gateway` serves as the factory for `HandlerChain` instances.
It also serves as the interface to HTTP servers.

## Creating a Gateway

```python
from rolo.gateway import Gateway

gateway = Gateway(
request_handlers=[
...
],
response_handlers=[
...
],
exception_handlers=[
...
],
finalizers=[
...
]
)
```

## Protocol adapters

You can use `rolo.gateway.wsgi` or `rolo.gateway.asgi` to expose a `Gateway` as either a WSGI or ASGI app.

Read more in the [serving](serving.md) section.
109 changes: 109 additions & 0 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
Getting started
===============

## Installation

Rolo is hosted on [pypi](https://pypi.org/project/rolo/) and can be installed via pip.

```sh
pip install rolo
```

## Hello World

Rolo provides different ways of building a web application.
It provides familiar concepts such as Router and `@route`, but also more flexible concepts like a Handler Chain.

### Router

Here is a simple [`Router`](router.md) that can be served as WSGI application using the Werkzeug dev server.
If you are familiar with Flask, `@route` works in a similar way.

```python
from werkzeug import Request
from werkzeug.serving import run_simple

from rolo import Router, route
from rolo.dispatcher import handler_dispatcher

@route("/")
def hello(request: Request):
return {"message": "Hello World"}

router = Router(dispatcher=handler_dispatcher())
router.add(hello)

run_simple("localhost", 8000, router.wsgi())
```

And to test:
```console
curl localhost:8000/
```
Should yield
```json
{"message": "Hello World"}
```

`rolo.Request` and `rolo.Response` objects work in the same way as Werkzeug's [Request / Response](https://werkzeug.palletsprojects.com/en/latest/wrappers/) wrappers.

### Gateway

A Gateway holds a set of handlers that are combined into a handler chain.
Here is a simple example with a single request handler that dynamically creates a response object similar to httpbin.

```python
from werkzeug.serving import run_simple

from rolo import Response
from rolo.gateway import Gateway, RequestContext, HandlerChain
from rolo.gateway.wsgi import WsgiGateway


def echo_handler(chain: HandlerChain, context: RequestContext, response: Response):
response.status_code = 200
response.set_json(
{
"method": context.request.method,
"path": context.request.path,
"query": context.request.args,
"headers": dict(context.request.headers),
}
)
chain.stop()


gateway = Gateway(
request_handlers=[echo_handler],
)

run_simple("localhost", 8000, WsgiGateway(gateway))
```

And to test:
```console
curl -s -X POST "localhost:8000/foo/bar?a=1&b=2" | jq .
```
Should give you:
```json
{
"method": "POST",
"path": "/foo/bar",
"query": {
"a": "1",
"b": "2"
},
"headers": {
"Host": "localhost:8000",
"User-Agent": "curl/7.81.0",
"Accept": "*/*"
}
}
```

## Next Steps

Learn how to
* Use the [Router](router.md)
* Use the [Handler Chain](handler_chain.md)
* [Serve](serving.md) rolo through your favorite web server
101 changes: 101 additions & 0 deletions docs/handler_chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
Handler Chain
=============

The rolo handler chain implements a variant of the chain-of-responsibility pattern to process an incoming HTTP request.
It is meant to be used together with a [`Gateway`](gateway.md), which is responsible for creating `HandlerChain` instances.

Handler chains are a powerful abstraction to create complex HTTP server behavior, while keeping code cleanly encapsulated and the high-level logic easy to understand.
You can find a simple example how to create a handler chain in the [Getting Started](getting_started.md) guide.

## Behavior

A handler chain consists of:
* request handlers: process the request and attempt to create an initial response
* response handlers: process the response
* finalizers: handlers that are always executed at the end of running a handler chain
* exception handlers: run when an exception occurs during the execution of a handler

Each HTTP request coming into the server has its own `HandlerChain` instance, since the handler chain holds state for the handling of a request.
A handler chain can be in three states that can be controlled by the handlers.

* Running - the implicit state in which _all_ handlers are executed sequentially
* Stopped - a handler has called `chain.stop()`. This stops the execution of all request handlers, and
proceeds immediately to executing the response handlers. Response handlers and finalizers will be run,
even if the chain has been stopped.
* Terminated - a handler has called `chain.terminate()`. This stops the execution of all request
handlers, and all response handlers, but runs the finalizers at the end.

If an exception occurs during the execution of request handlers, the chain by default stops the chain,
then runs each exception handler, and finally runs the response handlers.
Exceptions that happen during the execution of response or exception handlers are logged but do not modify the control flow of the chain.

## Handlers

Request handlers, response handlers, and finalizers need to satisfy the `Handler` protocol:

```python
from rolo import Response
from rolo.gateway import HandlerChain, RequestContext

def handle(chain: HandlerChain, context: RequestContext, response: Response):
...
```

* `chain`: the HandlerChain instance currently being executed. The handler implementation can call for example `chain.stop()` to indicate that it should skip all other request handlers.
* `context`: the RequestContext contains the rolo `Request` object, as well as a universal property store. You can simply call `context.myattr = ...` to pass a value down to the next handler
* `response`: Handlers of a handler chain don't return a response, instead the response being populated is handed down from handler to handler, and can thus be enriched

### Exception Handlers

Exception handlers are similar, only they are also passed the `Exception` that was raised in the handler chain.

```python
from rolo import Response
from rolo.gateway import HandlerChain, RequestContext

def handle(chain: HandlerChain, exception: Exception, context: RequestContext, response: Response):
...
```

## Builtin Handlers

### Router handler

Sometimes you have a `Gateway` but also want to use the [`Router`](router.md).
You can use the `RouterHandler` adapter to make a `Router` look like a handler chain `Handler`, and then pass it as handler to a Gateway.

```python
from rolo import Router
from rolo.gateway import Gateway
from rolo.gateway.handlers import RouterHandler

router: Router = ...
gateway: Gateway = ...

gateway.request_handlers.append(RouterHandler(router))
```

### Empty response handler

With the `EmptyResponseHandler` response handler automatically creates a default response if the response in the chain is empty.
By default, it creates an empty 404 response, but it can be customized:

```python
from rolo.gateway.handlers import EmptyResponseHandler

gateway.response_handlers.append(EmptyResponseHandler(status_code=404, body=b'404 Not Found'))
```

### Werkzeug exception handler

Werkzeug has a very useful [HTTP exception hierarchy](https://werkzeug.palletsprojects.com/en/latest/exceptions/) that can be used to programmatically trigger HTTP errors.
For instance, a request handler may raise a `NotFound` error.
To get the Gateway to automatically handle those exceptions and render them into JSON objects or HTML, you can use the `WerkzeugExceptionHandler`.

```python
from rolo.gateway.handlers import WerkzeugExceptionHandler

gateway.exception_handlers.append(WerkzeugExceptionHandler(output_format="json"))
```

In your request handler you can now raise any exception from `werkzeug.exceptions` and it will be rendered accordingly.
42 changes: 42 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Rolo documentation
==================

<p align="center">
<img src="https://github.com/thrau/rolo/assets/3996682/268786a8-6335-412f-bc72-8080f97cbb5a" alt="Rolo HTTP">
</p>
<p align="center">
<b>Rolo HTTP: A Python framework for building HTTP-based server applications.</b>
</p>

## Introduction

Rolo is a flexible framework and library to build HTTP-based server applications beyond microservices and REST APIs.
You can build HTTP-based RPC servers, websocket proxies, or other server types that typical web frameworks are not designed for.
Rolo was originally designed to build the AWS RPC protocol server in [LocalStack](https://github.com/localstack/localstack).

Rolo extends [Werkzeug](https://github.com/pallets/werkzeug/), a flexible Python HTTP server library, for you to use concepts you are familiar with like ``@route``, ``Request``, or ``Response``.
It introduces the concept of a ``Gateway`` and ``HandlerChain``, an implementation variant of the [chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern).

Rolo is designed for environments that do not use asyncio, but still require asynchronous HTTP features like HTTP2 SSE or Websockets.
To allow asynchronous communication, Rolo introduces an ASGI/WSGI bridge, that allows you to serve Rolo applications through ASGI servers like Hypercorn.

## Table of Content

```{toctree}
:caption: Quickstart
:maxdepth: 2
getting_started
```

```{toctree}
:caption: User Guide
:maxdepth: 2
router
handler_chain
gateway
websockets
serving
```

Loading

0 comments on commit c6f2cff

Please sign in to comment.