Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_newsfragments/1820.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added PEP 484 type annotations to WSGI, ASGI, and WebSocket tutorials and sample scripts.
14 changes: 7 additions & 7 deletions docs/user/tutorial-asgi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Here's how you can set up basic logging in your ASGI Falcon application via


class ErrorResource:
def on_get(self, req, resp):
async def on_get(self, req: falcon.asgi.Request, resp: falcon.asgi.Response) -> None:
raise Exception('Something went wrong!')


Expand Down Expand Up @@ -227,7 +227,7 @@ module, ``config.py`` next to ``app.py``, and add the following code to it:
DEFAULT_CONFIG_PATH = '/tmp/asgilook'
DEFAULT_UUID_GENERATOR = uuid.uuid4

def __init__(self):
def __init__(self) -> None:
self.storage_path = pathlib.Path(
os.environ.get('ASGI_LOOK_STORAGE_PATH', self.DEFAULT_CONFIG_PATH))
self.storage_path.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -355,20 +355,20 @@ of images. Place the code below in a file named ``images.py``:


class Images:
def __init__(self, config, store):
def __init__(self, config: Config, store: Store) -> None:
self._config = config
self._store = store

async def on_get(self, req, resp):
async def on_get(self, req: falcon.asgi.Request, resp: falcon.asgi.Response) -> None:
resp.media = [image.serialize() for image in self._store.list_images()]

async def on_get_image(self, req, resp, image_id):
async def on_get_image(self, req: falcon.asgi.Request, resp: falcon.asgi.Response, image_id: str) -> None:
# NOTE: image_id: UUID is converted back to a string identifier.
image = self._store.get(str(image_id))
resp.stream = await aiofiles.open(image.path, 'rb')
resp.content_type = falcon.MEDIA_JPEG

async def on_post(self, req, resp):
async def on_post(self, req: falcon.asgi.Request, resp: falcon.asgi.Response) -> None:
data = await req.stream.read()
image_id = str(self._config.uuid_generator())
image = await self._store.save(image_id, data)
Expand Down Expand Up @@ -465,7 +465,7 @@ Modify ``app.py`` to read as follows:
from .store import Store


def create_app(config=None):
def create_app(config: Config | None = None) -> falcon.asgi.App:
config = config or Config()
store = Store(config)
images = Images(config, store)
Expand Down
4 changes: 2 additions & 2 deletions docs/user/tutorial-websockets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ as expected.
app = falcon.asgi.App()

class HelloWorldResource:
async def on_get(self, req, resp):
async def on_get(self, req: falcon.asgi.Request, resp: falcon.asgi.Response) -> None:
resp.media = {'hello': 'world'}

app.add_route('/hello', HelloWorldResource())
Expand Down Expand Up @@ -105,7 +105,7 @@ let's keep it simple.


class EchoWebSocketResource:
async def on_websocket(self, req: Request, ws: WebSocket):
async def on_websocket(self, req: Request, ws: WebSocket) -> None:
try:
await ws.accept()
except WebSocketDisconnected:
Expand Down
36 changes: 18 additions & 18 deletions docs/user/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ and add the following code to it:

class Resource:

def on_get(self, req, resp):
def on_get(self, req: falcon.Request, resp: falcon.Response) -> None:
doc = {
'images': [
{
Expand Down Expand Up @@ -611,11 +611,11 @@ POSTs. Open ``images.py`` and add a POST responder to the
_CHUNK_SIZE_BYTES = 4096

# The resource object must now be initialized with a path used during POST
def __init__(self, storage_path):
def __init__(self, storage_path: str) -> None:
self._storage_path = storage_path

# This is the method we implemented before
def on_get(self, req, resp):
def on_get(self, req: falcon.Request, resp: falcon.Response) -> None:
doc = {
'images': [
{
Expand Down Expand Up @@ -731,10 +731,10 @@ operation:

class Resource:

def __init__(self, image_store):
def __init__(self, image_store: ImageStore) -> None:
self._image_store = image_store

def on_get(self, req, resp):
def on_get(self, req: falcon.Request, resp: falcon.Response) -> None:
doc = {
'images': [
{
Expand All @@ -747,7 +747,7 @@ operation:
resp.content_type = falcon.MEDIA_MSGPACK
resp.status = falcon.HTTP_200

def on_post(self, req, resp):
def on_post(self, req: falcon.Request, resp: falcon.Response) -> None:
name = self._image_store.save(req.stream, req.content_type)
resp.status = falcon.HTTP_201
resp.location = '/images/' + name
Expand Down Expand Up @@ -1136,10 +1136,10 @@ Go ahead and edit your ``images.py`` file to look something like this:

class Collection:

def __init__(self, image_store):
def __init__(self, image_store: ImageStore) -> None:
self._image_store = image_store

def on_get(self, req, resp):
def on_get(self, req: falcon.Request, resp: falcon.Response) -> None:
# TODO: Modify this to return a list of href's based on
# what images are actually available.
doc = {
Expand Down Expand Up @@ -1325,10 +1325,10 @@ terminal-friendly output. The top of file ``images.py`` should look like this:

class Collection:

def __init__(self, image_store):
def __init__(self, image_store: ImageStore) -> None:
self._image_store = image_store

def on_get(self, req, resp):
def on_get(self, req: falcon.Request, resp: falcon.Response) -> None:
# TODO: Modify this to return a list of href's based on
# what images are actually available.
doc = {
Expand Down Expand Up @@ -1386,10 +1386,10 @@ and also to enable a minimum value validation.

class Collection:

def __init__(self, image_store):
def __init__(self, image_store: ImageStore) -> None:
self._image_store = image_store

def on_get(self, req, resp):
def on_get(self, req: falcon.Request, resp: falcon.Response) -> None:
max_size = req.get_param_as_int("maxsize", min_value=1, default=-1)
images = self._image_store.list(max_size)
doc = {
Expand All @@ -1409,10 +1409,10 @@ and also to enable a minimum value validation.

class Item:

def __init__(self, image_store):
def __init__(self, image_store: ImageStore) -> None:
self._image_store = image_store

def on_get(self, req, resp, name):
def on_get(self, req: falcon.Request, resp: falcon.Response, name: str) -> None:
resp.content_type = mimetypes.guess_type(name)[0]
resp.stream, resp.content_length = self._image_store.open(name)

Expand Down Expand Up @@ -1546,7 +1546,7 @@ message. Add this method below the definition of ``ALLOWED_IMAGE_TYPES``:

.. code:: python

def validate_image_type(req, resp, resource, params):
def validate_image_type(req: falcon.Request, resp: falcon.Response, resource: object, params: dict[str, Any]) -> None:
if req.content_type not in ALLOWED_IMAGE_TYPES:
msg = 'Image type not allowed. Must be PNG, JPEG, or GIF'
raise falcon.HTTPBadRequest(title='Bad request', description=msg)
Expand Down Expand Up @@ -1576,7 +1576,7 @@ kwargs:

.. code:: python

def extract_project_id(req, resp, resource, params):
def extract_project_id(req: falcon.Request, resp: falcon.Response, resource: object, params: dict[str, Any]) -> None:
"""Adds `project_id` to the list of params for all responders.

Meant to be used as a `before` hook.
Expand Down Expand Up @@ -1671,10 +1671,10 @@ as follows:

class Item:

def __init__(self, image_store):
def __init__(self, image_store: ImageStore) -> None:
self._image_store = image_store

def on_get(self, req, resp, name):
def on_get(self, req: falcon.Request, resp: falcon.Response, name: str) -> None:
resp.content_type = mimetypes.guess_type(name)[0]

try:
Expand Down
4 changes: 3 additions & 1 deletion examples/asgilook/asgilook/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import falcon.asgi

from .cache import RedisCache
Expand All @@ -7,7 +9,7 @@
from .store import Store


def create_app(config=None):
def create_app(config: Config | None = None) -> falcon.asgi.App:
config = config or Config()
cache = RedisCache(config)
store = Store(config)
Expand Down
24 changes: 17 additions & 7 deletions examples/asgilook/asgilook/cache.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
from __future__ import annotations

from typing import Any
import msgpack

import falcon.asgi

from .config import Config


class RedisCache:
PREFIX = 'asgilook:'
INVALIDATE_ON = frozenset({'DELETE', 'POST', 'PUT'})
CACHE_HEADER = 'X-ASGILook-Cache'
TTL = 3600

def __init__(self, config):
def __init__(self, config: Config) -> None:
self._config = config
self._redis = self._config.redis_from_url(self._config.redis_host)

async def _serialize_response(self, resp):
async def _serialize_response(self, resp: falcon.asgi.Response) -> bytes:
data = await resp.render_body()
return msgpack.packb([resp.content_type, data], use_bin_type=True)

def _deserialize_response(self, resp, data):
def _deserialize_response(self, resp: falcon.asgi.Response, data: bytes) -> None:
resp.content_type, resp.data = msgpack.unpackb(data, raw=False)
resp.complete = True
resp.context.cached = True

async def process_startup(self, scope, event):
async def process_startup(self, scope: dict[str, Any], event: dict[str, Any]) -> None:
await self._redis.ping()

async def process_shutdown(self, scope, event):
async def process_shutdown(self, scope: dict[str, Any], event: dict[str, Any]) -> None:
await self._redis.aclose()

async def process_request(self, req, resp):

async def process_request(self, req: falcon.asgi.Request, resp: falcon.asgi.Response) -> None:
resp.context.cached = False

if req.method in self.INVALIDATE_ON:
Expand All @@ -40,7 +48,9 @@ async def process_request(self, req, resp):
else:
resp.set_header(self.CACHE_HEADER, 'Miss')

async def process_response(self, req, resp, resource, req_succeeded):
async def process_response(
self, req: falcon.asgi.Request, resp: falcon.asgi.Response, resource: object, req_succeeded: bool
) -> None:
if not req_succeeded:
return

Expand Down
2 changes: 1 addition & 1 deletion examples/asgilook/asgilook/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Config:
DEFAULT_REDIS_HOST = 'redis://localhost'
DEFAULT_UUID_GENERATOR = uuid.uuid4

def __init__(self):
def __init__(self) -> None:
self.storage_path = pathlib.Path(
os.environ.get('ASGI_LOOK_STORAGE_PATH', self.DEFAULT_CONFIG_PATH)
)
Expand Down
17 changes: 11 additions & 6 deletions examples/asgilook/asgilook/images.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
from __future__ import annotations

import aiofiles

import falcon

from .config import Config
from .store import Store


class Images:
def __init__(self, config, store):
def __init__(self, config: Config, store: Store) -> None:
self._config = config
self._store = store

async def on_get(self, req, resp):
async def on_get(self, req: falcon.asgi.Request, resp: falcon.asgi.Response) -> None:
resp.media = [image.serialize() for image in self._store.list_images()]

async def on_get_image(self, req, resp, image_id):
async def on_get_image(self, req: falcon.asgi.Request, resp: falcon.asgi.Response, image_id: str) -> None:
# NOTE: image_id: UUID is converted back to a string identifier.
image = self._store.get(str(image_id))
if not image:
Expand All @@ -20,7 +25,7 @@ async def on_get_image(self, req, resp, image_id):
resp.stream = await aiofiles.open(image.path, 'rb')
resp.content_type = falcon.MEDIA_JPEG

async def on_post(self, req, resp):
async def on_post(self, req: falcon.asgi.Request, resp: falcon.asgi.Response) -> None:
data = await req.stream.read()
image_id = str(self._config.uuid_generator())
image = await self._store.save(image_id, data)
Expand All @@ -31,10 +36,10 @@ async def on_post(self, req, resp):


class Thumbnails:
def __init__(self, store):
def __init__(self, store: Store) -> None:
self._store = store

async def on_get(self, req, resp, image_id, width, height):
async def on_get(self, req: falcon.asgi.Request, resp: falcon.asgi.Response, image_id: str, width: int, height: int) -> None:
image = self._store.get(str(image_id))
if not image:
raise falcon.HTTPNotFound
Expand Down
Loading
Loading