Skip to content

Commit 1e67faa

Browse files
authored
add pydoc docstrings throughout the codebase (#32)
1 parent 5607ef6 commit 1e67faa

13 files changed

Lines changed: 275 additions & 23 deletions

File tree

docs/router.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,10 @@ def update_item(request: Request, item_id: int, item: Item):
150150
return {"item_name": item.name, "item_id": item_id}
151151

152152

153-
router = Router()
153+
router = Router(dispatcher=handler_dispatcher())
154154
router.add(read_root)
155155
router.add(read_item)
156156
router.add(update_item)
157157
```
158+
159+
Pydantic support in the Router is automatically enabled if rolo finds that pydantic is installed.

rolo/client.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def __exit__(self, *args):
4141

4242
class _VerifyRespectingSession(requests.Session):
4343
"""
44-
A class which wraps requests.Session to circumvent https://github.com/psf/requests/issues/3829.
44+
A class which wraps ``requests.Session`` to circumvent https://github.com/psf/requests/issues/3829.
4545
This ensures that if `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE` are set, the request does not perform the TLS
46-
verification if `session.verify` is set to `False.
46+
verification if ``session.verify`` is set to ``False``.
4747
"""
4848

4949
def merge_environment_settings(self, url, proxies, stream, verify, *args, **kwargs):
@@ -56,10 +56,27 @@ def merge_environment_settings(self, url, proxies, stream, verify, *args, **kwar
5656

5757

5858
class SimpleRequestsClient(HttpClient):
59+
"""
60+
A ``HttpClient`` implementation that uses the ``requests`` library. Specifically it manages a ``requests.Session``
61+
object that is used to make HTTP requests according to the passed ``rolo.Request`` object.
62+
"""
63+
5964
session: requests.Session
6065
follow_redirects: bool
6166

6267
def __init__(self, session: requests.Session = None, follow_redirects: bool = True):
68+
"""
69+
Creates a new ``SimpleRequestsClient``. Use it to make HTTP requests with the requests library. Example use::
70+
71+
with SimpleRequestsClient() as client:
72+
response = client.request(Request("GET", "https://httpbin.org/get"))
73+
74+
You may also pass your own Session object, but note that will be closed if you call ``client.close()``.
75+
76+
:param session: An optional ``requests.Session`` object. If none is passed, one will be created. Note that
77+
calling ``client.close()`` will also close the session.
78+
:param follow_redirects: whether to follow HTTP redirects when making http calls.
79+
"""
6380
self.session = session or _VerifyRespectingSession()
6481
self.follow_redirects = follow_redirects
6582

@@ -97,7 +114,6 @@ def request(self, request: Request, server: str | None = None) -> Response:
97114
98115
:param request: the request to perform
99116
:param server: the URL to send the request to, which defaults to the host component of the original Request.
100-
:param allow_redirects: allow the request to follow redirects
101117
:return: the response.
102118
"""
103119

rolo/gateway/asgi.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
"""This module provides adapter code to expose a ``Gateway`` as an ASGI compatible application."""
12
import asyncio
23
import concurrent.futures.thread
34
from asyncio import AbstractEventLoop
45
from typing import Optional
56

67
from rolo.asgi import ASGIAdapter, ASGILifespanListener
8+
from rolo.websocket.adapter import WebSocketListener
79
from rolo.websocket.request import WebSocketRequest
810

911
from .gateway import Gateway
@@ -31,7 +33,8 @@ def _adjust_thread_count(self) -> None:
3133

3234
class AsgiGateway:
3335
"""
34-
Exposes a Gateway as an ASGI3 application. Under the hood, it uses a WsgiGateway with a threading async/sync bridge.
36+
Exposes a Gateway as an ASGI3 application. Under the hood, it uses a ``WsgiGateway`` with a threading async/sync
37+
bridge.
3538
"""
3639

3740
default_thread_count = 1000
@@ -44,8 +47,22 @@ def __init__(
4447
event_loop: Optional[AbstractEventLoop] = None,
4548
threads: int = None,
4649
lifespan_listener: Optional[ASGILifespanListener] = None,
47-
websocket_listener=None,
50+
websocket_listener: Optional[WebSocketListener] = None,
4851
) -> None:
52+
"""
53+
Wrap a ``Gateway`` and expose it as an ASGI3 application.
54+
55+
:param gateway: The Gateway instance to serve
56+
:param event_loop: optionally, you can pass your own event loop that is used by the gateway to process
57+
requests. By default, the global event loop via ``asyncio.get_event_loop()`` will be used.
58+
:param threads: Max number of threads used by the thread pool that is used to execute co-routines. Defaults to
59+
``AsgiGateway.default_thread_count`` set to 1000.
60+
:param lifespan_listener: Optional ``ASGILifespanListener`` callback that is called on ASGI webserver lifecycle
61+
events.
62+
:param websocket_listener: Optional ``WebSocketListener``, a rolo callback that handles incoming websocket
63+
connections. By default, the listener invokes ``Gateway.accept``, so there's rarely a reason you would need
64+
a custom one.
65+
"""
4966
self.gateway = gateway
5067

5168
self.event_loop = event_loop or asyncio.get_event_loop()

rolo/gateway/chain.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,31 @@ class RequestContext:
1818
"""
1919
A request context holds the original incoming HTTP Request and arbitrary data. It is passed through the handler
2020
chain and allows handlers to communicate.
21+
22+
You can use a ``RequestContext`` instance to store any attributes you want. Example::
23+
24+
def handle(chain: HandlerChain, context: RequestContext, response: Response):
25+
if context.request.headers.get("x-some-flag") == "true":
26+
context.some_flag = True
27+
else:
28+
context.some_flag = False
29+
30+
Note though that, unless ``some_flag`` was set earlier, accessing ``context.some_flag`` will raise an
31+
``AttributeError``. You can safely get the attribute via ``context.get("some_flag")``, which will returns
32+
``None`` if the attribute does not exist.
33+
34+
If you want type hints, you can subclass the ``RequestContext`` and then set the context class in your
35+
``Gateway``. Example::
36+
37+
class MyRequestContext(RequestContext):
38+
some_flag: bool
39+
40+
gateway = Gateway(context_class=MyRequestContext)
41+
2142
"""
2243

2344
request: Request
45+
"""The underlying HTTP request coming from the web server."""
2446

2547
def __init__(self, request: Request = None):
2648
self.request = request
@@ -36,6 +58,12 @@ def __getattr__(self, item):
3658
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{item}")
3759

3860
def get(self, key: str) -> t.Optional[t.Any]:
61+
"""
62+
Safely access an arbitrary attribute of the ``RequestContext``.
63+
64+
:param key: The key of the attribute (like ``some_flag``)
65+
:return: The value of the attribute, or None if the attribute does not exist.
66+
"""
3967
return self.__dict__.get(key)
4068

4169

@@ -277,12 +305,13 @@ def _call_exception_handlers(self, e, response):
277305
class CompositeHandler:
278306
"""
279307
A handler that sequentially invokes a list of Handlers, forming a stripped-down version of a handler
280-
chain.
308+
chain. Stop and termination conditions are determined by the ``HandlerChain`` instance that is being called.
281309
"""
282310

283311
handlers: list[Handler]
312+
"""List of handlers in this composite handler. Handlers are invoked in order they appear in the list."""
284313

285-
def __init__(self, return_on_stop=True) -> None:
314+
def __init__(self, return_on_stop: bool = True) -> None:
286315
"""
287316
Creates a new composite handler with an empty handler list.
288317
@@ -321,8 +350,8 @@ def __call__(self, chain: HandlerChain, context: RequestContext, response: Respo
321350

322351
class CompositeExceptionHandler:
323352
"""
324-
A exception handler that sequentially invokes a list of ExceptionHandler instances, forming a
325-
stripped-down version of a handler chain for exception handlers.
353+
An exception handler that sequentially invokes a list of ExceptionHandler instances, forming a
354+
stripped-down version of a handler chain for exception handlers. Works analogous to the ``CompositeHandler``.
326355
"""
327356

328357
handlers: t.List[ExceptionHandler]
@@ -361,7 +390,7 @@ def __call__(
361390

362391
class CompositeResponseHandler(CompositeHandler):
363392
"""
364-
A CompositeHandler that by default does not return on stop, meaning that all handlers in the composite
393+
A ``CompositeHandler`` that by default does not return on stop, meaning that all handlers in the composite
365394
will be executed, even if one of the handlers has called ``chain.stop()``. This mimics how response
366395
handlers are executed in the ``HandlerChain``.
367396
"""

rolo/gateway/gateway.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class Gateway:
1313
"""
14-
A gateway creates new HandlerChain instances for each request and processes requests through them.
14+
A Gateway creates new ``HandlerChain`` instances for each request and processes requests through them.
1515
"""
1616

1717
request_handlers: list[Handler]
@@ -35,6 +35,12 @@ def __init__(
3535
self.context_class = context_class or RequestContext
3636

3737
def new_chain(self) -> HandlerChain:
38+
"""
39+
Factory method for ``HandlerChain`` instances. This is called by ``process`` on every request, and can be
40+
overwritten by subclasses if they have custom ``HandlerChains``.
41+
42+
:return: A new HandlerChain instance to handle one single request/response cycle.
43+
"""
3844
return HandlerChain(
3945
self.request_handlers,
4046
self.response_handlers,
@@ -43,13 +49,26 @@ def new_chain(self) -> HandlerChain:
4349
)
4450

4551
def process(self, request: Request, response: Response):
52+
"""
53+
Called by the webserver integration, process creates a new ``HandlerChain``, a new ``RequestContext``, wraps
54+
the given request in the context, and then hands it to the handler chain via ``HandlerChain.handle``.
55+
56+
:param request: The HTTP request coming from the webserver.
57+
:param response: The response object to be populated for
58+
:return:
59+
"""
4660
chain = self.new_chain()
4761

4862
context = self.context_class(request)
4963

5064
chain.handle(context, response)
5165

5266
def accept(self, request: WebSocketRequest):
67+
"""
68+
Similar to ``process``, this method is called by webservers specifically for ``WebSocketRequest``.
69+
70+
:param request: The incoming websocket request.
71+
"""
5372
response = Response(status=101)
5473
self.process(request, response)
5574

rolo/gateway/handlers.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Several gateway handlers"""
22
import typing as t
33

4-
from werkzeug.datastructures import Headers
4+
from werkzeug.datastructures import Headers, MultiDict
55
from werkzeug.exceptions import HTTPException, NotFound
66

77
from rolo.response import Response
@@ -12,7 +12,15 @@
1212

1313
class RouterHandler:
1414
"""
15-
Adapter to serve a ``Router`` as a ``Handler``.
15+
Adapter to serve a ``Router`` as a ``Handler``. The handler takes from the ``RequestContext`` the ``Request``
16+
object, and dispatches it via ``Router.dispatch``. The ``Response`` object that call returns, is then merged into
17+
the ``Response`` object managed by the handler chain. If the router returns a response, the ``HandlerChain`` is
18+
stopped.
19+
20+
If the dispatching raises a ``NotFound`` (because there is no route in the Router to match the request), the chain
21+
will respond with 404 and "not found" as string, given that ``respond_not_found`` is set to True. This is to
22+
provide a simple, default way to handle 404 messages. In most cases, you will want your own 404 error handling
23+
in the handler chain, which is why ``respond_not_found`` is set to ``False`` by default.
1624
"""
1725

1826
router: Router
@@ -34,14 +42,40 @@ def __call__(self, chain: HandlerChain, context: RequestContext, response: Respo
3442

3543
class EmptyResponseHandler:
3644
"""
37-
Handler that creates a default response if the response in the context is empty.
45+
Handler that creates a default response if the response in the context is empty. A response is considered empty
46+
if its status code is set to 0 or None, and the response body is empty. Since ``Response`` is initialized with a
47+
200 status code by default, you'll have to explicitly set the status code to 0 or None in your handler chain.
48+
For example::
49+
50+
def init_response(chain, context, response):
51+
response.status_code = 0
52+
53+
def handle_request(chain, context, response):
54+
if context.request.path == "/hello"
55+
chain.respond("hello world")
56+
57+
gateway = Gateway(request_handlers=[
58+
init_response,
59+
handle_request,
60+
EmptyResponseHandler(404, body=b"not found")
61+
])
62+
63+
This handler chain will return 404 for all requests except those going to ``http://<server>/hello``.
3864
"""
3965

4066
status_code: int
4167
body: bytes
42-
headers: dict
68+
headers: t.Mapping[str, t.Any] | MultiDict[str, t.Any] | Headers
4369

4470
def __init__(self, status_code: int = 404, body: bytes = None, headers: Headers = None):
71+
"""
72+
Creates a new EmptyResponseHandler that will populate the ``Response`` object with the given values, if the
73+
response was previously considered empty.
74+
75+
:param status_code: The HTTP status code to use (defaults to 404)
76+
:param body: The body to use as response (defaults to empty string)
77+
:param headers: The additional headers to set for the response
78+
"""
4579
self.status_code = status_code
4680
self.body = body or b""
4781
self.headers = headers or Headers()
@@ -60,7 +94,52 @@ def populate_default_response(self, response: Response):
6094

6195

6296
class WerkzeugExceptionHandler:
97+
"""
98+
Convenience handler that translates werkzeug exceptions into HTML or JSON responses. Werkzeug exceptions are
99+
raised by ``Router`` instances, but can also be useful to use in your own handlers. These exceptions already
100+
contain a human-readable name, description, and an HTML template that can be rendered. The handler also supports
101+
a rolo-specific JSON format.
102+
103+
For example, this handler chain::
104+
105+
from werkzeug.exceptions import NotFound
106+
107+
def raise_not_found(chain, context, response):
108+
raise NotFound()
109+
110+
gateway = Gateway(
111+
request_handlers=[
112+
raise_not_found,
113+
],
114+
exception_handlers=[
115+
WerkzeugExceptionHandler(output_format="html"),
116+
]
117+
)
118+
119+
Would always yield the following HTML::
120+
121+
<!doctype html>
122+
<html lang=en>
123+
<title>404 Not Found</title>
124+
<h1>Not Found</h1>
125+
The requested URL was not found on the server. If you entered the URL manually please check
126+
your spelling and try again.
127+
128+
Or if you use JSON (via ``WerkzeugExceptionHandler(output_format="json")``)::
129+
130+
{
131+
"code": 404,
132+
"description": "The requested URL was not found on the server. [...]"
133+
}
134+
"""
135+
63136
def __init__(self, output_format: t.Literal["json", "html"] = None) -> None:
137+
"""
138+
Create a new ``WerkzeugExceptionHandler`` to use as exception handler in a handler chain.
139+
140+
:param output_format: The output format in which to render the exception into the response (either ``html``
141+
or ``json``), defaults to ``json``.
142+
"""
64143
self.format = output_format or "json"
65144

66145
def __call__(
@@ -78,6 +157,7 @@ def __call__(
78157
chain.respond(
79158
status_code=exception.code,
80159
headers=headers,
160+
# TODO: add name
81161
payload={"code": exception.code, "description": exception.description},
82162
)
83163
else:

rolo/gateway/wsgi.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"""This module contains adapter code that exposes a ``Gateway`` as a WSGI application."""
12
import typing as t
23

34
from werkzeug.datastructures import Headers, MultiDict
@@ -17,7 +18,9 @@
1718

1819
class WsgiGateway:
1920
"""
20-
Exposes a ``Gateway`` as a WSGI application.
21+
Exposes a ``Gateway`` as a WSGI application. This adapter creates from an incoming WSGIEnvironment dictionary a
22+
``Request`` object, as well as new ``Response`` object, and invokes ``Gateway.process(request, response)``.
23+
The populated ``Response`` object is then used to invoke the ``start_response`` handler.
2124
"""
2225

2326
gateway: Gateway
@@ -29,6 +32,14 @@ def __init__(self, gateway: Gateway) -> None:
2932
def __call__(
3033
self, environ: "WSGIEnvironment", start_response: "StartResponse"
3134
) -> t.Iterable[bytes]:
35+
"""
36+
Implements the WSGI application interface, which takes a WSGI environment dictionary, and the start response
37+
callback. These are all WSGI concepts.
38+
39+
:param environ: The WSGI environment.
40+
:param start_response: The WSGI StartResponse callback.
41+
:return:
42+
"""
3243
# create request from environment
3344
LOG.debug(
3445
"%s %s%s",

0 commit comments

Comments
 (0)