Source code for faust.web.views

"""Class-based views."""
from functools import wraps
from typing import (
    Any,
    Awaitable,
    Callable,
    ClassVar,
    Mapping,
    MutableMapping,
    Optional,
    Type,
    Union,
    cast,
    no_type_check,
)

from faust.types import AppT, ModelT
from faust.types.web import ViewDecorator, ViewHandlerFun
from yarl import URL

from . import exceptions
from .base import Request, Response, Web
from .exceptions import WebError

__all__ = ['View', 'gives_model', 'takes_model']

_bytes = bytes   # need alias for method named `bytes`


[docs]class View: """Web view (HTTP endpoint).""" ServerError: ClassVar[Type[WebError]] = exceptions.ServerError ValidationError: ClassVar[Type[WebError]] = exceptions.ValidationError ParseError: ClassVar[Type[WebError]] = exceptions.ParseError NotAuthenticated: ClassVar[Type[WebError]] = exceptions.NotAuthenticated PermissionDenied: ClassVar[Type[WebError]] = exceptions.PermissionDenied NotFound: ClassVar[Type[WebError]] = exceptions.NotFound view_name: str view_path: str methods: Mapping[str, Callable[[Request], Awaitable]]
[docs] @classmethod def from_handler(cls, fun: ViewHandlerFun) -> Type['View']: """Decorate ``async def`` handler function to create view.""" if not callable(fun): raise TypeError(f'View handler must be callable, not {fun!r}') return type(fun.__name__, (cls,), { 'get': fun, '__doc__': fun.__doc__, '__module__': fun.__module__, })
def __init__(self, app: AppT, web: Web) -> None: self.app = app self.web = web self.methods = { 'head': self.head, 'get': self.get, 'post': self.post, 'patch': self.patch, 'delete': self.delete, 'put': self.put, 'options': self.options, 'search': self.search, } self.__post_init__() def __post_init__(self) -> None: """Override this to add custom initialization to your view.""" ... async def __call__(self, request: Any) -> Any: """Perform HTTP request.""" return await self.dispatch(request)
[docs] async def dispatch(self, request: Any) -> Any: """Dispatch the request and perform any callbacks/cleanup.""" app = self.app sensors = app.sensors method = request.method.lower() kwargs = request.match_info or {} # XXX Aiohttp specific # we cast here since some subclasses take extra parameters # from the URL route (match_info). method = cast(Callable[..., Awaitable[Response]], self.methods[method]) sensor_state = sensors.on_web_request_start(app, request, view=self) response: Optional[Response] = None try: response = await method(cast(Request, request), **kwargs) except WebError as exc: response = await self.on_request_error(request, exc) finally: sensors.on_web_request_end( app, request, response, sensor_state, view=self) return response
[docs] async def on_request_error(self, request: Request, exc: WebError) -> Response: """Call when a request raises an exception.""" return self.error(exc.code, exc.detail, **exc.extra_context)
[docs] def path_for(self, view_name: str, **kwargs: Any) -> str: """Return the URL path for view by name. Supports match keyword arguments. """ return self.web.url_for(view_name, **kwargs)
[docs] def url_for(self, view_name: str, _base_url: Union[str, URL] = None, **kwargs: Any) -> URL: """Return the canonical URL for view by name. Supports match keyword arguments. Can take optional base name, which if not set will be the canonical URL of the app. """ if _base_url is None: _base_url = self.app.conf.canonical_url return URL('/'.join([ str(_base_url).rstrip('/'), str(self.path_for(view_name, **kwargs)).lstrip('/'), ]))
[docs] @no_type_check async def head(self, request: Request, **kwargs: Any) -> Any: """Override ``head`` to define the HTTP HEAD handler.""" return await self.get(request, **kwargs)
[docs] @no_type_check # subclasses change signature based on route match_info async def get(self, request: Request, **kwargs: Any) -> Any: """Override ``get`` to define the HTTP GET handler.""" raise exceptions.MethodNotAllowed('Method GET not allowed.')
[docs] @no_type_check # subclasses change signature based on route match_info async def post(self, request: Request, **kwargs: Any) -> Any: """Override ``post`` to define the HTTP POST handler.""" raise exceptions.MethodNotAllowed('Method POST not allowed.')
[docs] @no_type_check # subclasses change signature based on route match_info async def put(self, request: Request, **kwargs: Any) -> Any: """Override ``put`` to define the HTTP PUT handler.""" raise exceptions.MethodNotAllowed('Method PUT not allowed.')
[docs] @no_type_check # subclasses change signature based on route match_info async def patch(self, request: Request, **kwargs: Any) -> Any: """Override ``patch`` to define the HTTP PATCH handler.""" raise exceptions.MethodNotAllowed('Method PATCH not allowed.')
[docs] @no_type_check # subclasses change signature based on route match_info async def delete(self, request: Request, **kwargs: Any) -> Any: """Override ``delete`` to define the HTTP DELETE handler.""" raise exceptions.MethodNotAllowed('Method DELETE not allowed.')
[docs] @no_type_check # subclasses change signature based on route match_info async def options(self, request: Request, **kwargs: Any) -> Any: """Override ``options`` to define the HTTP OPTIONS handler.""" raise exceptions.MethodNotAllowed('Method OPTIONS not allowed.')
[docs] @no_type_check # subclasses change signature based on route match_info async def search(self, request: Request, **kwargs: Any) -> Any: """Override ``search`` to define the HTTP SEARCH handler.""" raise exceptions.MethodNotAllowed('Method SEARCH not allowed.')
[docs] def text(self, value: str, *, content_type: str = None, status: int = 200, reason: str = None, headers: MutableMapping = None) -> Response: """Create text response, using "text/plain" content-type.""" return self.web.text( value, content_type=content_type, status=status, reason=reason, headers=headers, )
[docs] def html(self, value: str, *, content_type: str = None, status: int = 200, reason: str = None, headers: MutableMapping = None) -> Response: """Create HTML response from string, ``text/html`` content-type.""" return self.web.html( value, content_type=content_type, status=status, reason=reason, headers=headers, )
[docs] def json(self, value: Any, *, content_type: str = None, status: int = 200, reason: str = None, headers: MutableMapping = None) -> Response: """Create new JSON response. Accepts any JSON-serializable value and will automatically serialize it for you. The content-type is set to "application/json". """ return self.web.json( value, content_type=content_type, status=status, reason=reason, headers=headers, )
[docs] def bytes(self, value: _bytes, *, content_type: str = None, status: int = 200, reason: str = None, headers: MutableMapping = None) -> Response: """Create new ``bytes`` response - for binary data.""" return self.web.bytes( value, content_type=content_type, status=status, reason=reason, headers=headers, )
[docs] async def read_request_content(self, request: Request) -> _bytes: """Return the request body as bytes.""" return await self.web.read_request_content(request)
[docs] def bytes_to_response(self, s: _bytes) -> Response: """Deserialize byte string back into a response object.""" return self.web.bytes_to_response(s)
[docs] def response_to_bytes(self, response: Response) -> _bytes: """Convert response to serializable byte string. The result is a byte string that can be deserialized using :meth:`bytes_to_response`. """ return self.web.response_to_bytes(response)
[docs] def route(self, pattern: str, handler: Callable) -> Any: """Create new route from pattern and handler.""" self.web.route(pattern, handler) return handler
[docs] def notfound(self, reason: str = 'Not Found', **kwargs: Any) -> Response: """Create not found error response. Deprecated: Use ``raise self.NotFound()`` instead. """ return self.error(404, reason, **kwargs)
[docs] def error(self, status: int, reason: str, **kwargs: Any) -> Response: """Create error JSON response.""" return self.json({'error': reason, **kwargs}, status=status)
[docs]def takes_model(Model: Type[ModelT]) -> ViewDecorator: """Decorate view function to return model data.""" def _decorate_view(fun: ViewHandlerFun) -> ViewHandlerFun: @wraps(fun) async def _inner(view: View, request: Request, *args: Any, **kwargs: Any) -> Response: data: bytes = await view.read_request_content(request) obj: ModelT = Model.loads(data, serializer='json') return await fun( # type: ignore view, request, obj, *args, **kwargs) return _inner return _decorate_view
[docs]def gives_model(Model: Type[ModelT]) -> ViewDecorator: """Decorate view function to automatically decode POST data. The POST data is decoded using the model you specify. """ def _decorate_view(fun: ViewHandlerFun) -> ViewHandlerFun: @wraps(fun) async def _inner(view: View, request: Request, *args: Any, **kwargs: Any) -> Response: response: Any response = await fun( # type: ignore view, request, *args, **kwargs) return view.json(response) return _inner return _decorate_view