Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
34 changes: 27 additions & 7 deletions examples/asgilook/asgilook/cache.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,46 @@
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 +54,13 @@ 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
30 changes: 24 additions & 6 deletions examples/asgilook/asgilook/images.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
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 +29,9 @@ 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 +42,17 @@ 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