From 33d7f4da6b05e5cc9d4596062712bbba3b401229 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 26 Jan 2021 23:14:47 +0200 Subject: [PATCH] Breakup App and Bluieprint --- sanic/app.py | 394 +------------------------------ sanic/blueprints.py | 475 ++++++------------------------------- sanic/mixins/__init__.py | 0 sanic/mixins/routes.py | 490 +++++++++++++++++++++++++++++++++++++++ sanic/models/__init__.py | 0 sanic/models/futures.py | 27 +++ sanic/router.py | 3 + 7 files changed, 600 insertions(+), 789 deletions(-) create mode 100644 sanic/mixins/__init__.py create mode 100644 sanic/mixins/routes.py create mode 100644 sanic/models/__init__.py create mode 100644 sanic/models/futures.py diff --git a/sanic/app.py b/sanic/app.py index 63490560..303b2f75 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -14,6 +14,8 @@ from traceback import format_exc from typing import Any, Dict, Iterable, List, Optional, Set, Type, Union from urllib.parse import urlencode, urlunparse +from sanic_routing.route import Route + from sanic import reloader_helpers from sanic.asgi import ASGIApp from sanic.blueprint_group import BlueprintGroup @@ -28,6 +30,8 @@ from sanic.exceptions import ( ) from sanic.handlers import ErrorHandler, ListenerType, MiddlewareType from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger +from sanic.mixins.routes import RouteMixin +from sanic.models.futures import FutureRoute from sanic.request import Request from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.router import Router @@ -43,7 +47,7 @@ from sanic.views import CompositionView from sanic.websocket import ConnectionClosed, WebSocketProtocol -class Sanic: +class Sanic(RouteMixin): _app_registry: Dict[str, "Sanic"] = {} test_mode = False @@ -59,6 +63,7 @@ class Sanic: configure_logging: bool = True, register: Optional[bool] = None, ) -> None: + super().__init__() # Get name from previous stack frame if name is None: @@ -161,391 +166,8 @@ class Sanic: return self.listener(event)(listener) - # Decorator - def route( - self, - uri, - methods=frozenset({"GET"}), - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ignore_body=False, - ): - """Decorate a function to be registered as a route - - :param uri: path of the URL - :param methods: list or tuple of methods allowed - :param host: - :param strict_slashes: - :param stream: - :param version: - :param name: user defined route name for url_for - :return: tuple of routes, decorated function - """ - - # Fix case where the user did not prefix the URL with a / - # and will probably get confused as to why it's not working - if not uri.startswith("/"): - uri = "/" + uri - - if strict_slashes is None: - strict_slashes = self.strict_slashes - - def response(handler): - if isinstance(handler, tuple): - # if a handler fn is already wrapped in a route, the handler - # variable will be a tuple of (existing routes, handler fn) - routes, handler = handler - else: - routes = [] - args = list(signature(handler).parameters.keys()) - - if not args: - handler_name = handler.__name__ - - raise ValueError( - f"Required parameter `request` missing " - f"in the {handler_name}() route?" - ) - - if stream: - handler.is_stream = stream - - self.router.add( - uri=uri, - methods=methods, - handler=handler, - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ignore_body=ignore_body, - ) - return routes, handler - - return response - - # Shorthand method decorators - def get( - self, - uri, - host=None, - strict_slashes=None, - version=None, - name=None, - ignore_body=True, - ): - """ - Add an API URL under the **GET** *HTTP* method - - :param uri: URL to be tagged to **GET** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`Sanic` to check if the request - URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"GET"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ignore_body=ignore_body, - ) - - def post( - self, - uri, - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ): - """ - Add an API URL under the **POST** *HTTP* method - - :param uri: URL to be tagged to **POST** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`Sanic` to check if the request - URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"POST"}), - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - ) - - def put( - self, - uri, - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ): - """ - Add an API URL under the **PUT** *HTTP* method - - :param uri: URL to be tagged to **PUT** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`Sanic` to check if the request - URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"PUT"}), - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - ) - - def head( - self, - uri, - host=None, - strict_slashes=None, - version=None, - name=None, - ignore_body=True, - ): - return self.route( - uri, - methods=frozenset({"HEAD"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ignore_body=ignore_body, - ) - - def options( - self, - uri, - host=None, - strict_slashes=None, - version=None, - name=None, - ignore_body=True, - ): - """ - Add an API URL under the **OPTIONS** *HTTP* method - - :param uri: URL to be tagged to **OPTIONS** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`Sanic` to check if the request - URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"OPTIONS"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ignore_body=ignore_body, - ) - - def patch( - self, - uri, - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ): - """ - Add an API URL under the **PATCH** *HTTP* method - - :param uri: URL to be tagged to **PATCH** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`Sanic` to check if the request - URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"PATCH"}), - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - ) - - def delete( - self, - uri, - host=None, - strict_slashes=None, - version=None, - name=None, - ignore_body=True, - ): - """ - Add an API URL under the **DELETE** *HTTP* method - - :param uri: URL to be tagged to **DELETE** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`Sanic` to check if the request - URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"DELETE"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ignore_body=ignore_body, - ) - - def add_route( - self, - handler, - uri, - methods=frozenset({"GET"}), - host=None, - strict_slashes=None, - version=None, - name=None, - stream=False, - ): - """A helper method to register class instance or - functions as a handler to the application url - routes. - - :param handler: function or class instance - :param uri: path of the URL - :param methods: list or tuple of methods allowed, these are overridden - if using a HTTPMethodView - :param host: - :param strict_slashes: - :param version: - :param name: user defined route name for url_for - :param stream: boolean specifying if the handler is a stream handler - :return: function or class instance - """ - # Handle HTTPMethodView differently - if hasattr(handler, "view_class"): - methods = set() - - for method in HTTP_METHODS: - _handler = getattr(handler.view_class, method.lower(), None) - if _handler: - methods.add(method) - if hasattr(_handler, "is_stream"): - stream = True - - # handle composition view differently - if isinstance(handler, CompositionView): - methods = handler.handlers.keys() - for _handler in handler.handlers.values(): - if hasattr(_handler, "is_stream"): - stream = True - break - - if strict_slashes is None: - strict_slashes = self.strict_slashes - - self.route( - uri=uri, - methods=methods, - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - )(handler) - return handler - - # Decorator - def websocket( - self, - uri, - host=None, - strict_slashes=None, - subprotocols=None, - version=None, - name=None, - ): - """ - Decorate a function to be registered as a websocket route - - :param uri: path of the URL - :param host: Host IP or FQDN details - :param strict_slashes: If the API endpoint needs to terminate - with a "/" or not - :param subprotocols: optional list of str with supported subprotocols - :param name: A unique name assigned to the URL so that it can - be used with :func:`url_for` - :return: tuple of routes, decorated function - """ - self.enable_websocket() - - # Fix case where the user did not prefix the URL with a / - # and will probably get confused as to why it's not working - if not uri.startswith("/"): - uri = "/" + uri - - if strict_slashes is None: - strict_slashes = self.strict_slashes - - def response(handler): - if isinstance(handler, tuple): - # if a handler fn is already wrapped in a route, the handler - # variable will be a tuple of (existing routes, handler fn) - routes, handler = handler - else: - routes = [] - websocket_handler = partial( - self._websocket_handler, handler, subprotocols=subprotocols - ) - websocket_handler.__name__ = ( - "websocket_handler_" + handler.__name__ - ) - websocket_handler.is_websocket = True - routes.extend( - self.router.add( - uri=uri, - handler=websocket_handler, - methods=frozenset({"GET"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ) - ) - return routes, handler - - return response + def _apply_route(self, route: FutureRoute) -> Route: + return self.router.add(**route._asdict()) def add_websocket_route( self, diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 09c54ffd..591c96f3 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -2,35 +2,18 @@ from collections import defaultdict, namedtuple from sanic.blueprint_group import BlueprintGroup from sanic.constants import HTTP_METHODS +from sanic.mixins.routes import RouteMixin +from sanic.models.futures import ( + FutureException, + FutureListener, + FutureMiddleware, + FutureRoute, + FutureStatic, +) from sanic.views import CompositionView -FutureRoute = namedtuple( - "FutureRoute", - [ - "handler", - "uri", - "methods", - "host", - "strict_slashes", - "stream", - "version", - "name", - ], -) -FutureListener = namedtuple( - "FutureListener", ["handler", "uri", "methods", "host"] -) -FutureMiddleware = namedtuple( - "FutureMiddleware", ["middleware", "args", "kwargs"] -) -FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"]) -FutureStatic = namedtuple( - "FutureStatic", ["uri", "file_or_directory", "args", "kwargs"] -) - - -class Blueprint: +class Blueprint(RouteMixin): def __init__( self, name, @@ -51,6 +34,8 @@ class Blueprint: :param strict_slashes: Enforce the API urls are requested with a training */* """ + super().__init__() + self.name = name self.url_prefix = url_prefix self.host = host @@ -64,6 +49,10 @@ class Blueprint: self.version = version self.strict_slashes = strict_slashes + def route(self, *args, **kwargs): + kwargs["apply"] = False + return super().route(*args, **kwargs) + @staticmethod def group(*blueprints, url_prefix=""): """ @@ -106,217 +95,80 @@ class Blueprint: routes = [] + # TODO: + # - Add BP name to handler name for all routes + # Routes - for future in self.routes: + for future in self._future_routes: # attach the blueprint name to the handler so that it can be # prefixed properly in the router future.handler.__blueprintname__ = self.name # Prepend the blueprint URI prefix if available uri = url_prefix + future.uri if url_prefix else future.uri - version = future.version or self.version - - _routes, _ = app.route( - uri=uri[1:] if uri.startswith("//") else uri, - methods=future.methods, - host=future.host or self.host, - strict_slashes=future.strict_slashes, - stream=future.stream, - version=version, - name=future.name, - )(future.handler) - if _routes: - routes += _routes - - for future in self.websocket_routes: - # attach the blueprint name to the handler so that it can be - # prefixed properly in the router - future.handler.__blueprintname__ = self.name - # Prepend the blueprint URI prefix if available - uri = url_prefix + future.uri if url_prefix else future.uri - _routes, _ = app.websocket( - uri=uri, - host=future.host or self.host, - strict_slashes=future.strict_slashes, - name=future.name, - )(future.handler) - if _routes: - routes += _routes - - # Static Files - for future in self.statics: - # Prepend the blueprint URI prefix if available - uri = url_prefix + future.uri if url_prefix else future.uri - _routes = app.static( - uri, future.file_or_directory, *future.args, **future.kwargs + apply_route = FutureRoute( + future.handler, + uri[1:] if uri.startswith("//") else uri, + future.methods, + future.host or self.host, + future.strict_slashes, + future.stream, + future.version or self.version, + future.name, + future.ignore_body, ) - if _routes: - routes += _routes - route_names = [route.name for route in routes if route] + _route = app._apply_route(apply_route) - # Middleware - for future in self.middlewares: - if future.args or future.kwargs: - app.register_named_middleware( - future.middleware, - route_names, - *future.args, - **future.kwargs, - ) - else: - app.register_named_middleware(future.middleware, route_names) + # TODO: + # for future in self.websocket_routes: + # # attach the blueprint name to the handler so that it can be + # # prefixed properly in the router + # future.handler.__blueprintname__ = self.name + # # Prepend the blueprint URI prefix if available + # uri = url_prefix + future.uri if url_prefix else future.uri + # _routes, _ = app.websocket( + # uri=uri, + # host=future.host or self.host, + # strict_slashes=future.strict_slashes, + # name=future.name, + # )(future.handler) + # if _routes: + # routes += _routes - # Exceptions - for future in self.exceptions: - app.exception(*future.args, **future.kwargs)(future.handler) + # # Static Files + # for future in self.statics: + # # Prepend the blueprint URI prefix if available + # uri = url_prefix + future.uri if url_prefix else future.uri + # _routes = app.static( + # uri, future.file_or_directory, *future.args, **future.kwargs + # ) + # if _routes: + # routes += _routes + + # route_names = [route.name for route in routes if route] + + # # Middleware + # for future in self.middlewares: + # if future.args or future.kwargs: + # app.register_named_middleware( + # future.middleware, + # route_names, + # *future.args, + # **future.kwargs, + # ) + # else: + # app.register_named_middleware(future.middleware, route_names) + + # # Exceptions + # for future in self.exceptions: + # app.exception(*future.args, **future.kwargs)(future.handler) # Event listeners for event, listeners in self.listeners.items(): for listener in listeners: app.listener(event)(listener) - def route( - self, - uri, - methods=frozenset({"GET"}), - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ): - """Create a blueprint route from a decorated function. - - :param uri: endpoint at which the route will be accessible. - :param methods: list of acceptable HTTP methods. - :param host: IP Address of FQDN for the sanic server to use. - :param strict_slashes: Enforce the API urls are requested with a - training */* - :param stream: If the route should provide a streaming support - :param version: Blueprint Version - :param name: Unique name to identify the Route - - :return a decorated method that when invoked will return an object - of type :class:`FutureRoute` - """ - if strict_slashes is None: - strict_slashes = self.strict_slashes - - def decorator(handler): - route = FutureRoute( - handler, - uri, - methods, - host, - strict_slashes, - stream, - version, - name, - ) - self.routes.append(route) - return handler - - return decorator - - def add_route( - self, - handler, - uri, - methods=frozenset({"GET"}), - host=None, - strict_slashes=None, - version=None, - name=None, - stream=False, - ): - """Create a blueprint route from a function. - - :param handler: function for handling uri requests. Accepts function, - or class instance with a view_class method. - :param uri: endpoint at which the route will be accessible. - :param methods: list of acceptable HTTP methods. - :param host: IP Address of FQDN for the sanic server to use. - :param strict_slashes: Enforce the API urls are requested with a - training */* - :param version: Blueprint Version - :param name: user defined route name for url_for - :param stream: boolean specifying if the handler is a stream handler - :return: function or class instance - """ - # Handle HTTPMethodView differently - if hasattr(handler, "view_class"): - methods = set() - - for method in HTTP_METHODS: - if getattr(handler.view_class, method.lower(), None): - methods.add(method) - - if strict_slashes is None: - strict_slashes = self.strict_slashes - - # handle composition view differently - if isinstance(handler, CompositionView): - methods = handler.handlers.keys() - - self.route( - uri=uri, - methods=methods, - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - )(handler) - return handler - - def websocket( - self, uri, host=None, strict_slashes=None, version=None, name=None - ): - """Create a blueprint websocket route from a decorated function. - - :param uri: endpoint at which the route will be accessible. - :param host: IP Address of FQDN for the sanic server to use. - :param strict_slashes: Enforce the API urls are requested with a - training */* - :param version: Blueprint Version - :param name: Unique name to identify the Websocket Route - """ - if strict_slashes is None: - strict_slashes = self.strict_slashes - - def decorator(handler): - nonlocal uri - nonlocal host - nonlocal strict_slashes - nonlocal version - nonlocal name - - name = f"{self.name}.{name or handler.__name__}" - route = FutureRoute( - handler, uri, [], host, strict_slashes, False, version, name - ) - self.websocket_routes.append(route) - return handler - - return decorator - - def add_websocket_route( - self, handler, uri, host=None, version=None, name=None - ): - """Create a blueprint websocket route from a function. - - :param handler: function for handling uri requests. Accepts function, - or class instance with a view_class method. - :param uri: endpoint at which the route will be accessible. - :param host: IP Address of FQDN for the sanic server to use. - :param version: Blueprint Version - :param name: Unique name to identify the Websocket Route - :return: function or class instance - """ - self.websocket(uri=uri, host=host, version=version, name=name)(handler) - return handler - def listener(self, event): """Create a listener from a decorated function. @@ -395,186 +247,3 @@ class Blueprint: static = FutureStatic(uri, file_or_directory, args, kwargs) self.statics.append(static) - - # Shorthand method decorators - def get( - self, uri, host=None, strict_slashes=None, version=None, name=None - ): - """ - Add an API URL under the **GET** *HTTP* method - - :param uri: URL to be tagged to **GET** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`sanic.app.Sanic` to check - if the request URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"GET"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ) - - def post( - self, - uri, - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ): - """ - Add an API URL under the **POST** *HTTP* method - - :param uri: URL to be tagged to **POST** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`sanic.app.Sanic` to check - if the request URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"POST"}), - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - ) - - def put( - self, - uri, - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ): - """ - Add an API URL under the **PUT** *HTTP* method - - :param uri: URL to be tagged to **PUT** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`sanic.app.Sanic` to check - if the request URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"PUT"}), - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - ) - - def head( - self, uri, host=None, strict_slashes=None, version=None, name=None - ): - """ - Add an API URL under the **HEAD** *HTTP* method - - :param uri: URL to be tagged to **HEAD** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`sanic.app.Sanic` to check - if the request URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"HEAD"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ) - - def options( - self, uri, host=None, strict_slashes=None, version=None, name=None - ): - """ - Add an API URL under the **OPTIONS** *HTTP* method - - :param uri: URL to be tagged to **OPTIONS** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`sanic.app.Sanic` to check - if the request URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"OPTIONS"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ) - - def patch( - self, - uri, - host=None, - strict_slashes=None, - stream=False, - version=None, - name=None, - ): - """ - Add an API URL under the **PATCH** *HTTP* method - - :param uri: URL to be tagged to **PATCH** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`sanic.app.Sanic` to check - if the request URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"PATCH"}), - host=host, - strict_slashes=strict_slashes, - stream=stream, - version=version, - name=name, - ) - - def delete( - self, uri, host=None, strict_slashes=None, version=None, name=None - ): - """ - Add an API URL under the **DELETE** *HTTP* method - - :param uri: URL to be tagged to **DELETE** method of *HTTP* - :param host: Host IP or FQDN for the service to use - :param strict_slashes: Instruct :class:`sanic.app.Sanic` to check - if the request URLs need to terminate with a */* - :param version: API Version - :param name: Unique name that can be used to identify the Route - :return: Object decorated with :func:`route` method - """ - return self.route( - uri, - methods=frozenset({"DELETE"}), - host=host, - strict_slashes=strict_slashes, - version=version, - name=name, - ) diff --git a/sanic/mixins/__init__.py b/sanic/mixins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py new file mode 100644 index 00000000..d97a3d6b --- /dev/null +++ b/sanic/mixins/routes.py @@ -0,0 +1,490 @@ +from functools import partial +from inspect import signature +from typing import List, Set + +import websockets + +from sanic_routing.route import Route + +from sanic.constants import HTTP_METHODS +from sanic.models.futures import FutureRoute +from sanic.views import CompositionView + + +class RouteMixin: + def __init__(self) -> None: + self._future_routes: Set[Route] = set() + self._future_websocket_routes: Set[Route] = set() + + def _apply_route(self, route: FutureRoute) -> Route: + raise NotImplementedError + + def _route( + self, + uri, + methods=frozenset({"GET"}), + host=None, + strict_slashes=None, + stream=False, + version=None, + name=None, + ignore_body=False, + apply=True, + subprotocols=None, + websocket=False, + ): + """Create a blueprint route from a decorated function. + + :param uri: endpoint at which the route will be accessible. + :param methods: list of acceptable HTTP methods. + :param host: IP Address of FQDN for the sanic server to use. + :param strict_slashes: Enforce the API urls are requested with a + training */* + :param stream: If the route should provide a streaming support + :param version: Blueprint Version + :param name: Unique name to identify the Route + + :return a decorated method that when invoked will return an object + of type :class:`FutureRoute` + """ + + if websocket: + self.enable_websocket() + + # Fix case where the user did not prefix the URL with a / + # and will probably get confused as to why it's not working + if not uri.startswith("/"): + uri = "/" + uri + + if strict_slashes is None: + strict_slashes = self.strict_slashes + + def decorator(handler): + nonlocal uri + nonlocal methods + nonlocal host + nonlocal strict_slashes + nonlocal stream + nonlocal version + nonlocal name + nonlocal ignore_body + nonlocal subprotocols + nonlocal websocket + + if isinstance(handler, tuple): + # if a handler fn is already wrapped in a route, the handler + # variable will be a tuple of (existing routes, handler fn) + _, handler = handler + + if websocket: + websocket_handler = partial( + self._websocket_handler, + handler, + subprotocols=subprotocols, + ) + websocket_handler.__name__ = ( + "websocket_handler_" + handler.__name__ + ) + websocket_handler.is_websocket = True + handler = websocket_handler + + # TODO: + # - THink this thru.... do we want all routes namespaced? + # - + name = self._generate_name(handler, name) + + route = FutureRoute( + handler, + uri, + methods, + host, + strict_slashes, + stream, + version, + name, + ignore_body, + ) + + self._future_routes.add(route) + + args = list(signature(handler).parameters.keys()) + if websocket and len(args) < 2: + handler_name = handler.__name__ + + raise ValueError( + f"Required parameter `request` and/or `ws` missing " + f"in the {handler_name}() route?" + ) + elif not args: + handler_name = handler.__name__ + + raise ValueError( + f"Required parameter `request` missing " + f"in the {handler_name}() route?" + ) + + if not websocket and stream: + handler.is_stream = stream + + if apply: + self._apply_route(route) + + return route, handler + + return decorator + + def route( + self, + uri, + methods=frozenset({"GET"}), + host=None, + strict_slashes=None, + stream=False, + version=None, + name=None, + ignore_body=False, + apply=True, + ): + return self._route( + uri=uri, + methods=methods, + host=host, + strict_slashes=strict_slashes, + stream=stream, + version=version, + name=name, + ignore_body=ignore_body, + apply=apply, + ) + + def add_route( + self, + handler, + uri, + methods=frozenset({"GET"}), + host=None, + strict_slashes=None, + version=None, + name=None, + stream=False, + ): + """A helper method to register class instance or + functions as a handler to the application url + routes. + + :param handler: function or class instance + :param uri: path of the URL + :param methods: list or tuple of methods allowed, these are overridden + if using a HTTPMethodView + :param host: + :param strict_slashes: + :param version: + :param name: user defined route name for url_for + :param stream: boolean specifying if the handler is a stream handler + :return: function or class instance + """ + # Handle HTTPMethodView differently + if hasattr(handler, "view_class"): + methods = set() + + for method in HTTP_METHODS: + _handler = getattr(handler.view_class, method.lower(), None) + if _handler: + methods.add(method) + if hasattr(_handler, "is_stream"): + stream = True + + # handle composition view differently + if isinstance(handler, CompositionView): + methods = handler.handlers.keys() + for _handler in handler.handlers.values(): + if hasattr(_handler, "is_stream"): + stream = True + break + + if strict_slashes is None: + strict_slashes = self.strict_slashes + + self.route( + uri=uri, + methods=methods, + host=host, + strict_slashes=strict_slashes, + stream=stream, + version=version, + name=name, + )(handler) + return handler + + # Shorthand method decorators + def get( + self, + uri, + host=None, + strict_slashes=None, + version=None, + name=None, + ignore_body=True, + ): + """ + Add an API URL under the **GET** *HTTP* method + + :param uri: URL to be tagged to **GET** method of *HTTP* + :param host: Host IP or FQDN for the service to use + :param strict_slashes: Instruct :class:`Sanic` to check if the request + URLs need to terminate with a */* + :param version: API Version + :param name: Unique name that can be used to identify the Route + :return: Object decorated with :func:`route` method + """ + return self.route( + uri, + methods=frozenset({"GET"}), + host=host, + strict_slashes=strict_slashes, + version=version, + name=name, + ignore_body=ignore_body, + ) + + def post( + self, + uri, + host=None, + strict_slashes=None, + stream=False, + version=None, + name=None, + ): + """ + Add an API URL under the **POST** *HTTP* method + + :param uri: URL to be tagged to **POST** method of *HTTP* + :param host: Host IP or FQDN for the service to use + :param strict_slashes: Instruct :class:`Sanic` to check if the request + URLs need to terminate with a */* + :param version: API Version + :param name: Unique name that can be used to identify the Route + :return: Object decorated with :func:`route` method + """ + return self.route( + uri, + methods=frozenset({"POST"}), + host=host, + strict_slashes=strict_slashes, + stream=stream, + version=version, + name=name, + ) + + def put( + self, + uri, + host=None, + strict_slashes=None, + stream=False, + version=None, + name=None, + ): + """ + Add an API URL under the **PUT** *HTTP* method + + :param uri: URL to be tagged to **PUT** method of *HTTP* + :param host: Host IP or FQDN for the service to use + :param strict_slashes: Instruct :class:`Sanic` to check if the request + URLs need to terminate with a */* + :param version: API Version + :param name: Unique name that can be used to identify the Route + :return: Object decorated with :func:`route` method + """ + return self.route( + uri, + methods=frozenset({"PUT"}), + host=host, + strict_slashes=strict_slashes, + stream=stream, + version=version, + name=name, + ) + + def head( + self, + uri, + host=None, + strict_slashes=None, + version=None, + name=None, + ignore_body=True, + ): + return self.route( + uri, + methods=frozenset({"HEAD"}), + host=host, + strict_slashes=strict_slashes, + version=version, + name=name, + ignore_body=ignore_body, + ) + + def options( + self, + uri, + host=None, + strict_slashes=None, + version=None, + name=None, + ignore_body=True, + ): + """ + Add an API URL under the **OPTIONS** *HTTP* method + + :param uri: URL to be tagged to **OPTIONS** method of *HTTP* + :param host: Host IP or FQDN for the service to use + :param strict_slashes: Instruct :class:`Sanic` to check if the request + URLs need to terminate with a */* + :param version: API Version + :param name: Unique name that can be used to identify the Route + :return: Object decorated with :func:`route` method + """ + return self.route( + uri, + methods=frozenset({"OPTIONS"}), + host=host, + strict_slashes=strict_slashes, + version=version, + name=name, + ignore_body=ignore_body, + ) + + def patch( + self, + uri, + host=None, + strict_slashes=None, + stream=False, + version=None, + name=None, + ): + """ + Add an API URL under the **PATCH** *HTTP* method + + :param uri: URL to be tagged to **PATCH** method of *HTTP* + :param host: Host IP or FQDN for the service to use + :param strict_slashes: Instruct :class:`Sanic` to check if the request + URLs need to terminate with a */* + :param version: API Version + :param name: Unique name that can be used to identify the Route + :return: Object decorated with :func:`route` method + """ + return self.route( + uri, + methods=frozenset({"PATCH"}), + host=host, + strict_slashes=strict_slashes, + stream=stream, + version=version, + name=name, + ) + + def delete( + self, + uri, + host=None, + strict_slashes=None, + version=None, + name=None, + ignore_body=True, + ): + """ + Add an API URL under the **DELETE** *HTTP* method + + :param uri: URL to be tagged to **DELETE** method of *HTTP* + :param host: Host IP or FQDN for the service to use + :param strict_slashes: Instruct :class:`Sanic` to check if the request + URLs need to terminate with a */* + :param version: API Version + :param name: Unique name that can be used to identify the Route + :return: Object decorated with :func:`route` method + """ + return self.route( + uri, + methods=frozenset({"DELETE"}), + host=host, + strict_slashes=strict_slashes, + version=version, + name=name, + ignore_body=ignore_body, + ) + + def websocket( + self, + uri, + host=None, + strict_slashes=None, + version=None, + name=None, + subprotocols=None, + apply: bool = True, + ): + """Create a blueprint websocket route from a decorated function. + + :param uri: endpoint at which the route will be accessible. + :param host: IP Address of FQDN for the sanic server to use. + :param strict_slashes: Enforce the API urls are requested with a + training */* + :param version: Blueprint Version + :param name: Unique name to identify the Websocket Route + """ + return self._route( + uri=uri, + host=host, + methods=None, + strict_slashes=strict_slashes, + version=version, + name=name, + apply=apply, + subprotocols=subprotocols, + websocket=True, + ) + + def add_websocket_route( + self, + handler, + uri, + host=None, + strict_slashes=None, + subprotocols=None, + version=None, + name=None, + ): + """ + A helper method to register a function as a websocket route. + + :param handler: a callable function or instance of a class + that can handle the websocket request + :param host: Host IP or FQDN details + :param uri: URL path that will be mapped to the websocket + handler + handler + :param strict_slashes: If the API endpoint needs to terminate + with a "/" or not + :param subprotocols: Subprotocols to be used with websocket + handshake + :param name: A unique name assigned to the URL so that it can + be used with :func:`url_for` + :return: Objected decorated by :func:`websocket` + """ + if strict_slashes is None: + strict_slashes = self.strict_slashes + + return self.websocket( + uri, + host=host, + strict_slashes=strict_slashes, + subprotocols=subprotocols, + version=version, + name=name, + )(handler) + + def _generate_name(self, handler, name: str) -> str: + return name or handler.__name__ diff --git a/sanic/models/__init__.py b/sanic/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sanic/models/futures.py b/sanic/models/futures.py new file mode 100644 index 00000000..94ff9ddf --- /dev/null +++ b/sanic/models/futures.py @@ -0,0 +1,27 @@ +from collections import namedtuple + + +FutureRoute = namedtuple( + "FutureRoute", + [ + "handler", + "uri", + "methods", + "host", + "strict_slashes", + "stream", + "version", + "name", + "ignore_body", + ], +) +FutureListener = namedtuple( + "FutureListener", ["handler", "uri", "methods", "host"] +) +FutureMiddleware = namedtuple( + "FutureMiddleware", ["middleware", "args", "kwargs"] +) +FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"]) +FutureStatic = namedtuple( + "FutureStatic", ["uri", "file_or_directory", "args", "kwargs"] +) diff --git a/sanic/router.py b/sanic/router.py index aef8aa82..9217243a 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -40,6 +40,7 @@ class Router(BaseRouter): handler, host=None, strict_slashes=False, + stream=False, ignore_body=False, version=None, name=None, @@ -48,6 +49,7 @@ class Router(BaseRouter): # - host # - strict_slashes # - ignore_body + # - stream if version is not None: version = str(version).strip("/").lstrip("v") uri = "/".join([f"/v{version}", uri.lstrip("/")]) @@ -56,5 +58,6 @@ class Router(BaseRouter): path=uri, handler=handler, methods=methods, name=name ) route.ctx.ignore_body = ignore_body + route.ctx.stream = stream return route