add versioning
This commit is contained in:
parent
c181eb0539
commit
4265ad5f23
50
docs/sanic/versioning.md
Normal file
50
docs/sanic/versioning.md
Normal 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
|
||||||
|
```
|
44
sanic/app.py
44
sanic/app.py
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user