add versioning

This commit is contained in:
Raphael Deem 2017-07-12 20:18:56 -07:00
parent c181eb0539
commit 4265ad5f23
6 changed files with 170 additions and 45 deletions

50
docs/sanic/versioning.md Normal file
View File

@ -0,0 +1,50 @@
# Versioning
You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number.
## Per route
You can pass a version number to the routes directly.
```python
from sanic import response
@app.route('/text', verion=1)
def handle_request(request):
return response.text('Hello world! Version 1')
@app.route('/text', verion=2)
def handle_request(request):
return response.text('Hello world! Version 2')
app.run(port=80)
```
Then with curl:
```bash
curl localhost/v1/text
curl localhost/v2/text
```
## Global blueprint version
You can also pass a version number to the blueprint, which will apply to all routes.
```python
from sanic import response
from sanic.blueprints import Blueprint
bp = Blueprint('test', version=1)
@bp.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
```
Then with curl:
```bash
curl localhost/v1/html
```

View File

@ -113,7 +113,7 @@ class Sanic:
# Decorator # Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None, def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False, stream=False): strict_slashes=False, stream=False, version=None):
"""Decorate a function to be registered as a route """Decorate a function to be registered as a route
:param uri: path of the URL :param uri: path of the URL
@ -136,42 +136,49 @@ class Sanic:
if stream: if stream:
handler.is_stream = stream handler.is_stream = stream
self.router.add(uri=uri, methods=methods, handler=handler, self.router.add(uri=uri, methods=methods, handler=handler,
host=host, strict_slashes=strict_slashes) host=host, strict_slashes=strict_slashes,
version=version)
return handler return handler
return response return response
# Shorthand method decorators # Shorthand method decorators
def get(self, uri, host=None, strict_slashes=False): def get(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=frozenset({"GET"}), host=host, return self.route(uri, methods=frozenset({"GET"}), host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, version=version)
def post(self, uri, host=None, strict_slashes=False, stream=False): def post(self, uri, host=None, strict_slashes=False, stream=False,
version=None):
return self.route(uri, methods=frozenset({"POST"}), host=host, return self.route(uri, methods=frozenset({"POST"}), host=host,
strict_slashes=strict_slashes, stream=stream) strict_slashes=strict_slashes, stream=stream,
version=version)
def put(self, uri, host=None, strict_slashes=False, stream=False): def put(self, uri, host=None, strict_slashes=False, stream=False,
version=None):
return self.route(uri, methods=frozenset({"PUT"}), host=host, return self.route(uri, methods=frozenset({"PUT"}), host=host,
strict_slashes=strict_slashes, stream=stream) strict_slashes=strict_slashes, stream=stream,
version=version)
def head(self, uri, host=None, strict_slashes=False): def head(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=frozenset({"HEAD"}), host=host, return self.route(uri, methods=frozenset({"HEAD"}), host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, version=version)
def options(self, uri, host=None, strict_slashes=False): def options(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, version=version)
def patch(self, uri, host=None, strict_slashes=False, stream=False): def patch(self, uri, host=None, strict_slashes=False, stream=False,
version=None):
return self.route(uri, methods=frozenset({"PATCH"}), host=host, return self.route(uri, methods=frozenset({"PATCH"}), host=host,
strict_slashes=strict_slashes, stream=stream) strict_slashes=strict_slashes, stream=stream,
version=version)
def delete(self, uri, host=None, strict_slashes=False): def delete(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=frozenset({"DELETE"}), host=host, return self.route(uri, methods=frozenset({"DELETE"}), host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, version=version)
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False): strict_slashes=False, version=None):
"""A helper method to register class instance or """A helper method to register class instance or
functions as a handler to the application url functions as a handler to the application url
routes. routes.
@ -204,7 +211,8 @@ class Sanic:
break break
self.route(uri=uri, methods=methods, host=host, self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes, stream=stream)(handler) strict_slashes=strict_slashes, stream=stream,
version=version)(handler)
return handler return handler
# Decorator # Decorator

View File

@ -4,8 +4,8 @@ from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView from sanic.views import CompositionView
FutureRoute = namedtuple('Route', FutureRoute = namedtuple('Route',
['handler', 'uri', 'methods', ['handler', 'uri', 'methods', 'host',
'host', 'strict_slashes', 'stream']) 'strict_slashes', 'stream', 'version'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
@ -14,7 +14,7 @@ FutureStatic = namedtuple('Route',
class Blueprint: class Blueprint:
def __init__(self, name, url_prefix=None, host=None): def __init__(self, name, url_prefix=None, host=None, version=None):
"""Create a new blueprint """Create a new blueprint
:param name: unique name of the blueprint :param name: unique name of the blueprint
@ -30,6 +30,7 @@ class Blueprint:
self.listeners = defaultdict(list) self.listeners = defaultdict(list)
self.middlewares = [] self.middlewares = []
self.statics = [] self.statics = []
self.version = version
def register(self, app, options): def register(self, app, options):
"""Register the blueprint to the sanic app.""" """Register the blueprint to the sanic app."""
@ -43,12 +44,16 @@ class Blueprint:
future.handler.__blueprintname__ = self.name future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available # Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri uri = url_prefix + future.uri if url_prefix else future.uri
version = future.version or self.version
app.route( app.route(
uri=uri[1:] if uri.startswith('//') else uri, uri=uri[1:] if uri.startswith('//') else uri,
methods=future.methods, methods=future.methods,
host=future.host or self.host, host=future.host or self.host,
strict_slashes=future.strict_slashes, strict_slashes=future.strict_slashes,
stream=future.stream stream=future.stream,
version=version
)(future.handler) )(future.handler)
for future in self.websocket_routes: for future in self.websocket_routes:
@ -89,7 +94,7 @@ class Blueprint:
app.listener(event)(listener) app.listener(event)(listener)
def route(self, uri, methods=frozenset({'GET'}), host=None, def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False, stream=False): strict_slashes=False, stream=False, version=None):
"""Create a blueprint route from a decorated function. """Create a blueprint route from a decorated function.
:param uri: endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
@ -97,13 +102,13 @@ class Blueprint:
""" """
def decorator(handler): def decorator(handler):
route = FutureRoute( route = FutureRoute(
handler, uri, methods, host, strict_slashes, stream) handler, uri, methods, host, strict_slashes, stream, version)
self.routes.append(route) self.routes.append(route)
return handler return handler
return decorator return decorator
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False): strict_slashes=False, version=None):
"""Create a blueprint route from a function. """Create a blueprint route from a function.
:param handler: function for handling uri requests. Accepts function, :param handler: function for handling uri requests. Accepts function,
@ -125,21 +130,22 @@ class Blueprint:
methods = handler.handlers.keys() methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host, self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes)(handler) strict_slashes=strict_slashes, version=version)(handler)
return handler return handler
def websocket(self, uri, host=None, strict_slashes=False): def websocket(self, uri, host=None, strict_slashes=False, version=None):
"""Create a blueprint websocket route from a decorated function. """Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
""" """
def decorator(handler): def decorator(handler):
route = FutureRoute(handler, uri, [], host, strict_slashes, False) route = FutureRoute(handler, uri, [], host, strict_slashes,
False, version)
self.websocket_routes.append(route) self.websocket_routes.append(route)
return handler return handler
return decorator return decorator
def add_websocket_route(self, handler, uri, host=None): def add_websocket_route(self, handler, uri, host=None, version=None):
"""Create a blueprint websocket route from a function. """Create a blueprint websocket route from a function.
:param handler: function for handling uri requests. Accepts function, :param handler: function for handling uri requests. Accepts function,
@ -147,7 +153,7 @@ class Blueprint:
:param uri: endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:return: function or class instance :return: function or class instance
""" """
self.websocket(uri=uri, host=host)(handler) self.websocket(uri=uri, host=host, version=version)(handler)
return handler return handler
def listener(self, event): def listener(self, event):
@ -193,30 +199,36 @@ class Blueprint:
self.statics.append(static) self.statics.append(static)
# Shorthand method decorators # Shorthand method decorators
def get(self, uri, host=None, strict_slashes=False): def get(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=["GET"], host=host, return self.route(uri, methods=["GET"], host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, version=version)
def post(self, uri, host=None, strict_slashes=False, stream=False): def post(self, uri, host=None, strict_slashes=False, stream=False,
version=None):
return self.route(uri, methods=["POST"], host=host, return self.route(uri, methods=["POST"], host=host,
strict_slashes=strict_slashes, stream=stream) strict_slashes=strict_slashes, stream=stream,
version=version)
def put(self, uri, host=None, strict_slashes=False, stream=False): def put(self, uri, host=None, strict_slashes=False, stream=False,
version=None):
return self.route(uri, methods=["PUT"], host=host, return self.route(uri, methods=["PUT"], host=host,
strict_slashes=strict_slashes, stream=stream) strict_slashes=strict_slashes, stream=stream,
version=version)
def head(self, uri, host=None, strict_slashes=False): def head(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=["HEAD"], host=host, return self.route(uri, methods=["HEAD"], host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, version=version)
def options(self, uri, host=None, strict_slashes=False): def options(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=["OPTIONS"], host=host, return self.route(uri, methods=["OPTIONS"], host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, version=version)
def patch(self, uri, host=None, strict_slashes=False, stream=False): def patch(self, uri, host=None, strict_slashes=False, stream=False,
version=None):
return self.route(uri, methods=["PATCH"], host=host, return self.route(uri, methods=["PATCH"], host=host,
strict_slashes=strict_slashes, stream=stream) strict_slashes=strict_slashes, stream=stream,
version=version)
def delete(self, uri, host=None, strict_slashes=False): def delete(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=["DELETE"], host=host, return self.route(uri, methods=["DELETE"], host=host,
strict_slashes=strict_slashes) strict_slashes=strict_slashes, version=version)

View File

@ -98,7 +98,8 @@ class Router:
return name, _type, pattern return name, _type, pattern
def add(self, uri, methods, handler, host=None, strict_slashes=False): def add(self, uri, methods, handler, host=None, strict_slashes=False,
version=None):
"""Add a handler to the route list """Add a handler to the route list
:param uri: path to match :param uri: path to match
@ -107,8 +108,15 @@ class Router:
:param handler: request handler function. :param handler: request handler function.
When executed, it should provide a response object. When executed, it should provide a response object.
:param strict_slashes: strict to trailing slash :param strict_slashes: strict to trailing slash
:param version: current version of the route or blueprint. See
docs for further details.
:return: Nothing :return: Nothing
""" """
if version is not None:
if uri.startswith('/'):
uri = "/".join(["/v{}".format(str(version)), uri[1:]])
else:
uri = "/".join(["/v{}".format(str(version)), uri])
# add regular version # add regular version
self._add(uri, methods, handler, host) self._add(uri, methods, handler, host)

View File

@ -1,16 +1,42 @@
import asyncio import asyncio
import inspect import inspect
import pytest
from sanic import Sanic from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.response import json, text from sanic.response import json, text
from sanic.exceptions import NotFound, ServerError, InvalidUsage from sanic.exceptions import NotFound, ServerError, InvalidUsage
from sanic.constants import HTTP_METHODS
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# GET # GET
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get')
bp = Blueprint('test_text')
method = method.lower()
func = getattr(bp, method)
if callable(func):
@func('/{}'.format(method), version=1)
def handler(request):
return text('OK')
else:
print(func)
raise
app.blueprint(bp)
client_method = getattr(app.test_client, method)
request, response = client_method('/v1/{}'.format(method))
assert response.status == 200
def test_bp(): def test_bp():
app = Sanic('test_text') app = Sanic('test_text')
bp = Blueprint('test_text') bp = Blueprint('test_text')

View File

@ -4,12 +4,33 @@ import pytest
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.router import RouteExists, RouteDoesNotExist from sanic.router import RouteExists, RouteDoesNotExist
from sanic.constants import HTTP_METHODS
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# UTF-8 # UTF-8
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get')
method = method.lower()
func = getattr(app, method)
if callable(func):
@func('/{}'.format(method), version=1)
def handler(request):
return text('OK')
else:
print(func)
raise
client_method = getattr(app.test_client, method)
request, response = client_method('/v1/{}'.format(method))
assert response.status== 200
def test_shorthand_routes_get(): def test_shorthand_routes_get():
app = Sanic('test_shorhand_routes_get') app = Sanic('test_shorhand_routes_get')