Clarify, reformat, and add to documentation guides (#318)
* Reorder and clarify the 'Request Data' guide, adding a section on RequestParameters * Clarify routing guide, adding introduction and HTTP types sections * Clarify the use-cases of middleware * Clean up formatting in the exceptions guide and add some common exceptions. * Fix formatting of blueprints and add use-case example. * Clarify the class-based views guide * Clarify and fix formatting of cookies guide * Clarify static files guide * Clarify the custom protocols guide. * Add more information to the deploying guide * Fix broken list in the community extensions list. * Add introduction and improve warning to contributing guide * Expand getting started guide * Reorder guides and add links between them * Standardise heading capitalisation
This commit is contained in:
parent
f77bb81def
commit
6176964bdf
|
@ -1,25 +1,20 @@
|
||||||
# Blueprints
|
# Blueprints
|
||||||
|
|
||||||
Blueprints are objects that can be used for sub-routing within an application.
|
Blueprints are objects that can be used for sub-routing within an application.
|
||||||
Instead of adding routes to the application object, blueprints define similar
|
Instead of adding routes to the application instance, blueprints define similar
|
||||||
methods for adding routes, which are then registered with the application in a
|
methods for adding routes, which are then registered with the application in a
|
||||||
flexible and pluggable manner.
|
flexible and pluggable manner.
|
||||||
|
|
||||||
## Why?
|
Blueprints are especially useful for larger applications, where your
|
||||||
|
application logic can be broken down into several groups or areas of
|
||||||
Blueprints are especially useful for larger applications, where your application
|
responsibility.
|
||||||
logic can be broken down into several groups or areas of responsibility.
|
|
||||||
|
|
||||||
It is also useful for API versioning, where one blueprint may point at
|
|
||||||
`/v1/<routes>`, and another pointing at `/v2/<routes>`.
|
|
||||||
|
|
||||||
|
|
||||||
## My First Blueprint
|
## My First Blueprint
|
||||||
|
|
||||||
The following shows a very simple blueprint that registers a handler-function at
|
The following shows a very simple blueprint that registers a handler-function at
|
||||||
the root `/` of your application.
|
the root `/` of your application.
|
||||||
|
|
||||||
Suppose you save this file as `my_blueprint.py`, this can be imported in your
|
Suppose you save this file as `my_blueprint.py`, which can be imported into your
|
||||||
main application later.
|
main application later.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -34,7 +29,8 @@ async def bp_root(request):
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Registering Blueprints
|
## Registering blueprints
|
||||||
|
|
||||||
Blueprints must be registered with the application.
|
Blueprints must be registered with the application.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -48,14 +44,19 @@ app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
```
|
```
|
||||||
|
|
||||||
This will add the blueprint to the application and register any routes defined
|
This will add the blueprint to the application and register any routes defined
|
||||||
by that blueprint.
|
by that blueprint. In this example, the registered routes in the `app.router`
|
||||||
In this example, the registered routes in the `app.router` will look like:
|
will look like:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Middleware
|
## Using blueprints
|
||||||
|
|
||||||
|
Blueprints have much the same functionality as an application instance.
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
Using blueprints allows you to also register middleware globally.
|
Using blueprints allows you to also register middleware globally.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -72,30 +73,36 @@ async def halt_response(request, response):
|
||||||
return text('I halted the response')
|
return text('I halted the response')
|
||||||
```
|
```
|
||||||
|
|
||||||
## Exceptions
|
### Exceptions
|
||||||
Exceptions can also be applied exclusively to blueprints globally.
|
|
||||||
|
Exceptions can be applied exclusively to blueprints globally.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@bp.exception(NotFound)
|
@bp.exception(NotFound)
|
||||||
def ignore_404s(request, exception):
|
def ignore_404s(request, exception):
|
||||||
return text("Yep, I totally found the page: {}".format(request.url))
|
return text("Yep, I totally found the page: {}".format(request.url))
|
||||||
|
```
|
||||||
|
|
||||||
## Static files
|
### Static files
|
||||||
Static files can also be served globally, under the blueprint prefix.
|
|
||||||
|
Static files can be served globally, under the blueprint prefix.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
bp.static('/folder/to/serve', '/web/path')
|
bp.static('/folder/to/serve', '/web/path')
|
||||||
```
|
```
|
||||||
|
|
||||||
## Start and Stop
|
## Start and stop
|
||||||
Blueprints and run functions during the start and stop process of the server.
|
|
||||||
If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork
|
Blueprints can run functions during the start and stop process of the server.
|
||||||
|
If running in multiprocessor mode (more than 1 worker), these are triggered
|
||||||
|
after the workers fork.
|
||||||
|
|
||||||
Available events are:
|
Available events are:
|
||||||
|
|
||||||
* before_server_start - Executed before the server begins to accept connections
|
- `before_server_start`: Executed before the server begins to accept connections
|
||||||
* after_server_start - Executed after the server begins to accept connections
|
- `after_server_start`: Executed after the server begins to accept connections
|
||||||
* before_server_stop - Executed before the server stops accepting connections
|
- `before_server_stop`: Executed before the server stops accepting connections
|
||||||
* after_server_stop - Executed after the server is stopped and all requests are complete
|
- `after_server_stop`: Executed after the server is stopped and all requests are complete
|
||||||
|
|
||||||
```python
|
```python
|
||||||
bp = Blueprint('my_blueprint')
|
bp = Blueprint('my_blueprint')
|
||||||
|
@ -109,3 +116,49 @@ async def setup_connection():
|
||||||
async def close_connection():
|
async def close_connection():
|
||||||
await database.close()
|
await database.close()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Use-case: API versioning
|
||||||
|
|
||||||
|
Blueprints can be very useful for API versioning, where one blueprint may point
|
||||||
|
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
|
||||||
|
|
||||||
|
When a blueprint is initialised, it can take an optional `url_prefix` argument,
|
||||||
|
which will be prepended to all routes defined on the blueprint. This feature
|
||||||
|
can be used to implement our API versioning scheme.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# blueprints.py
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic import Blueprint
|
||||||
|
|
||||||
|
blueprint_v1 = Blueprint('v1')
|
||||||
|
blueprint_v2 = Blueprint('v2')
|
||||||
|
|
||||||
|
@blueprint_v1.route('/')
|
||||||
|
async def api_v1_root(request):
|
||||||
|
return text('Welcome to version 1 of our documentation')
|
||||||
|
|
||||||
|
@blueprint_v2.route('/')
|
||||||
|
async def api_v2_root(request):
|
||||||
|
return text('Welcome to version 2 of our documentation')
|
||||||
|
```
|
||||||
|
|
||||||
|
When we register our blueprints on the app, the routes `/v1` and `/v2` will now
|
||||||
|
point to the individual blueprints, which allows the creation of *sub-sites*
|
||||||
|
for each API version.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# main.py
|
||||||
|
from sanic import Sanic
|
||||||
|
from blueprints import blueprint_v1, blueprint_v2
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
app.blueprint(blueprint_v1)
|
||||||
|
app.blueprint(blueprint_v2)
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Previous:** [Exceptions](exceptions.html)
|
||||||
|
|
||||||
|
**Next:** [Class-based views](class_based_views.html)
|
||||||
|
|
|
@ -1,8 +1,25 @@
|
||||||
# Class based views
|
# Class-Based Views
|
||||||
|
|
||||||
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response.
|
Class-based views are simply classes which implement response behaviour to
|
||||||
|
requests. They provide a way to compartmentalise handling of different HTTP
|
||||||
|
request types at the same endpoint. Rather than defining and decorating three
|
||||||
|
different handler functions, one for each of an endpoint's supported request
|
||||||
|
type, the endpoint can be assigned a class-based view.
|
||||||
|
|
||||||
|
## Defining views
|
||||||
|
|
||||||
|
A class-based view should subclass `HTTPMethodView`. You can then implement
|
||||||
|
class methods for every HTTP request type you want to support. If a request is
|
||||||
|
received that has no defined method, a `405: Method not allowed` response will
|
||||||
|
be generated.
|
||||||
|
|
||||||
|
To register a class-based view on an endpoint, the `app.add_route` method is
|
||||||
|
used. The first argument should be the defined class with the method `as_view`
|
||||||
|
invoked, and the second should be the URL endpoint.
|
||||||
|
|
||||||
|
The available methods are `get`, `post`, `put`, `patch`, and `delete`. A class
|
||||||
|
using all these methods would look like the following.
|
||||||
|
|
||||||
## Examples
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
|
@ -10,7 +27,6 @@ from sanic.response import text
|
||||||
|
|
||||||
app = Sanic('some_name')
|
app = Sanic('some_name')
|
||||||
|
|
||||||
|
|
||||||
class SimpleView(HTTPMethodView):
|
class SimpleView(HTTPMethodView):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
@ -32,7 +48,10 @@ app.add_route(SimpleView.as_view(), '/')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If you need any url params just mention them in method definition:
|
## URL parameters
|
||||||
|
|
||||||
|
If you need any URL parameters, as discussed in the routing guide, include them
|
||||||
|
in the method definition.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class NameView(HTTPMethodView):
|
class NameView(HTTPMethodView):
|
||||||
|
@ -41,10 +60,12 @@ class NameView(HTTPMethodView):
|
||||||
return text('Hello {}'.format(name))
|
return text('Hello {}'.format(name))
|
||||||
|
|
||||||
app.add_route(NameView.as_view(), '/<name>')
|
app.add_route(NameView.as_view(), '/<name>')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to add decorator for class, you could set decorators variable
|
## Decorators
|
||||||
|
|
||||||
|
If you want to add any decorators to the class, you can set the `decorators`
|
||||||
|
class variable. These will be applied to the class when `as_view` is called.
|
||||||
|
|
||||||
```
|
```
|
||||||
class ViewWithDecorator(HTTPMethodView):
|
class ViewWithDecorator(HTTPMethodView):
|
||||||
|
@ -54,5 +75,8 @@ class ViewWithDecorator(HTTPMethodView):
|
||||||
return text('Hello I have a decorator')
|
return text('Hello I have a decorator')
|
||||||
|
|
||||||
app.add_route(ViewWithDecorator.as_view(), '/url')
|
app.add_route(ViewWithDecorator.as_view(), '/url')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Previous:** [Blueprints](blueprints.html)
|
||||||
|
|
||||||
|
**Next:** [Cookies](cookies.html)
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
# How to contribute to Sanic
|
# Contributing
|
||||||
|
|
||||||
Thank you for your interest!
|
Thank you for your interest! Sanic is always looking for contributors. If you
|
||||||
|
don't feel comfortable contributing code, adding docstrings to the source files
|
||||||
|
is very appreciated.
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
* `python -m pip install pytest`
|
* `python -m pip install pytest`
|
||||||
* `python -m pytest tests`
|
* `python -m pytest tests`
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Sanic's documentation is built using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in Markdown and can be found in the `docs` folder, while the module reference is automatically generated using `sphinx-apidoc`.
|
Sanic's documentation is built
|
||||||
|
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
|
||||||
|
Markdown and can be found in the `docs` folder, while the module reference is
|
||||||
|
automatically generated using `sphinx-apidoc`.
|
||||||
|
|
||||||
To generate the documentation from scratch:
|
To generate the documentation from scratch:
|
||||||
|
|
||||||
|
@ -20,4 +26,10 @@ sphinx-build -b html docs docs/_build
|
||||||
The HTML documentation will be created in the `docs/_build` folder.
|
The HTML documentation will be created in the `docs/_build` folder.
|
||||||
|
|
||||||
## Warning
|
## Warning
|
||||||
One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged.
|
|
||||||
|
One of the main goals of Sanic is speed. Code that lowers the performance of
|
||||||
|
Sanic without significant gains in usability, security, or features may not be
|
||||||
|
merged. Please don't let this intimidate you! If you have any concerns about an
|
||||||
|
idea, open an issue for discussion and help.
|
||||||
|
|
||||||
|
**Previous:** [Sanic extensions](extensions.html)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# Cookies
|
# Cookies
|
||||||
|
|
||||||
## Request
|
Cookies are pieces of data which persist inside a user's browser. Sanic can
|
||||||
|
both read and write cookies, which are stored as key-value pairs.
|
||||||
|
|
||||||
Request cookies can be accessed via the request.cookie dictionary
|
## Reading cookies
|
||||||
|
|
||||||
### Example
|
A user's cookies can be accessed `Request` object's `cookie` dictionary.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route("/cookie")
|
@app.route("/cookie")
|
||||||
|
@ -16,28 +16,11 @@ async def test(request):
|
||||||
return text("Test cookie set to: {}".format(test_cookie))
|
return text("Test cookie set to: {}".format(test_cookie))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Response
|
## Writing cookies
|
||||||
|
|
||||||
Response cookies can be set like dictionary values and
|
When returning a response, cookies can be set on the `Response` object.
|
||||||
have the following parameters available:
|
|
||||||
|
|
||||||
* expires - datetime - Time for cookie to expire on the client's browser
|
|
||||||
* path - string - The Path attribute specifies the subset of URLs to
|
|
||||||
which this cookie applies
|
|
||||||
* comment - string - Cookie comment (metadata)
|
|
||||||
* domain - string - Specifies the domain for which the
|
|
||||||
cookie is valid. An explicitly specified domain must always
|
|
||||||
start with a dot.
|
|
||||||
* max-age - number - Number of seconds the cookie should live for
|
|
||||||
* secure - boolean - Specifies whether the cookie will only be sent via
|
|
||||||
HTTPS
|
|
||||||
* httponly - boolean - Specifies whether the cookie cannot be read
|
|
||||||
by javascript
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route("/cookie")
|
@app.route("/cookie")
|
||||||
|
@ -48,3 +31,22 @@ async def test(request):
|
||||||
response.cookies['test']['httponly'] = True
|
response.cookies['test']['httponly'] = True
|
||||||
return response
|
return response
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Response cookies can be set like dictionary values and have the following
|
||||||
|
parameters available:
|
||||||
|
|
||||||
|
- `expires` (datetime): The time for the cookie to expire on the
|
||||||
|
client's browser.
|
||||||
|
- `path` (string): The subset of URLs to which this cookie applies.
|
||||||
|
- `comment` (string): A comment (metadata).
|
||||||
|
- `domain` (string): Specifies the domain for which the cookie is valid. An
|
||||||
|
explicitly specified domain must always start with a dot.
|
||||||
|
- `max-age` (number): Number of seconds the cookie should live for.
|
||||||
|
- `secure` (boolean): Specifies whether the cookie will only be sent via
|
||||||
|
HTTPS.
|
||||||
|
- `httponly` (boolean): Specifies whether the cookie cannot be read by
|
||||||
|
Javascript.
|
||||||
|
|
||||||
|
**Previous:** [Class-based views](class_based_views.html)
|
||||||
|
|
||||||
|
**Next:** [Custom protocols](custom_protocol.html)
|
||||||
|
|
|
@ -1,34 +1,36 @@
|
||||||
# Custom Protocol
|
# Custom Protocols
|
||||||
|
|
||||||
You can change the behavior of protocol by using custom protocol.
|
*Note: this is advanced usage, and most readers will not need such functionality.*
|
||||||
If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic.
|
|
||||||
|
|
||||||
* loop
|
You can change the behavior of Sanic's protocol by specifying a custom
|
||||||
`loop` is an asyncio compatible event loop.
|
protocol, which should be a subclass
|
||||||
|
of
|
||||||
|
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||||
|
This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method.
|
||||||
|
|
||||||
* connections
|
The constructor of the custom protocol class receives the following keyword
|
||||||
`connections` is a `set object` to store protocol objects.
|
arguments from Sanic.
|
||||||
When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections.
|
|
||||||
|
|
||||||
* signal
|
- `loop`: an `asyncio`-compatible event loop.
|
||||||
`signal` is a `sanic.server.Signal object` with `stopped attribute`.
|
- `connections`: a `set` to store protocol objects. When Sanic receives
|
||||||
When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`.
|
`SIGINT` or `SIGTERM`, it executes `protocol.close_if_idle` for all protocol
|
||||||
|
objects stored in this set.
|
||||||
* request_handler
|
- `signal`: a `sanic.server.Signal` object with the `stopped` attribute. When
|
||||||
`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments.
|
Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` is assigned `True`.
|
||||||
|
- `request_handler`: a coroutine that takes a `sanic.request.Request` object
|
||||||
* error_handler
|
and a `response` callback as arguments.
|
||||||
`error_handler` is a `sanic.exceptions.Handler` object.
|
- `error_handler`: a `sanic.exceptions.Handler` which is called when exceptions
|
||||||
|
are raised.
|
||||||
* request_timeout
|
- `request_timeout`: the number of seconds before a request times out.
|
||||||
`request_timeout` is seconds for timeout.
|
- `request_max_size`: an integer specifying the maximum size of a request, in bytes.
|
||||||
|
|
||||||
* request_max_size
|
|
||||||
`request_max_size` is bytes of max request size.
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.
|
|
||||||
In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`.
|
An error occurs in the default protocol if a handler function does not return
|
||||||
|
an `HTTPResponse` object.
|
||||||
|
|
||||||
|
By overriding the `write_response` protocol method, if a handler returns a
|
||||||
|
string it will be converted to an `HTTPResponse object`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
@ -68,3 +70,7 @@ async def response(request):
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Previous:** [Cookies](cookies.html)
|
||||||
|
|
||||||
|
**Next:** [Testing](testing.html)
|
||||||
|
|
|
@ -1,35 +1,59 @@
|
||||||
# Deploying
|
# Deploying
|
||||||
|
|
||||||
When it comes to deploying Sanic, there's not much to it, but there are
|
Deploying Sanic is made simple by the inbuilt webserver. After defining an
|
||||||
a few things to take note of.
|
instance of `sanic.Sanic`, we can call the `run` method with the following
|
||||||
|
keyword arguments:
|
||||||
|
|
||||||
|
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
||||||
|
- `port` *(default `8000`)*: Port to host the server on.
|
||||||
|
- `debug` *(default `False`)*: Enables debug output (slows server).
|
||||||
|
- `before_start` *(default `None`)*: Function or list of functions to be executed
|
||||||
|
before the server starts accepting connections.
|
||||||
|
- `after_start` *(default `None`)*: Function or list of functions to be executed
|
||||||
|
after the server starts accepting connections.
|
||||||
|
- `before_stop` *(default `None`)*: Function or list of functions to be
|
||||||
|
executed when a stop signal is received before it is
|
||||||
|
respected.
|
||||||
|
- `after_stop` *(default `None`)*: Function or list of functions to be executed
|
||||||
|
when all requests are complete.
|
||||||
|
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
|
||||||
|
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
||||||
|
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
||||||
|
- `loop` *(default `None`)*: An `asyncio`-compatible event loop. If none is
|
||||||
|
specified, Sanic creates its own event loop.
|
||||||
|
- `protocol` *(default `HttpProtocol`)*: Subclass
|
||||||
|
of
|
||||||
|
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||||
|
|
||||||
## Workers
|
## Workers
|
||||||
|
|
||||||
By default, Sanic listens in the main process using only 1 CPU core.
|
By default, Sanic listens in the main process using only one CPU core. To crank
|
||||||
To crank up the juice, just specify the number of workers in the run
|
up the juice, just specify the number of workers in the `run` arguments.
|
||||||
arguments like so:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||||
```
|
```
|
||||||
|
|
||||||
Sanic will automatically spin up multiple processes and route
|
Sanic will automatically spin up multiple processes and route traffic between
|
||||||
traffic between them. We recommend as many workers as you have
|
them. We recommend as many workers as you have available cores.
|
||||||
available cores.
|
|
||||||
|
|
||||||
## Running via Command
|
## Running via command
|
||||||
|
|
||||||
If you like using command line arguments, you can launch a sanic server
|
If you like using command line arguments, you can launch a Sanic server by
|
||||||
by executing the module. For example, if you initialized sanic as
|
executing the module. For example, if you initialized Sanic as `app` in a file
|
||||||
app in a file named server.py, you could run the server like so:
|
named `server.py`, you could run the server like so:
|
||||||
|
|
||||||
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
|
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
|
||||||
|
|
||||||
With this way of running sanic, it is not necessary to run app.run in
|
With this way of running sanic, it is not necessary to invoke `app.run` in your
|
||||||
your python file. If you do, just make sure you wrap it in name == main
|
Python file. If you do, make sure you wrap it so that it only executes when
|
||||||
like so:
|
directly run by the interpreter.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Previous:** [Request Data](request_data.html)
|
||||||
|
|
||||||
|
**Next:** [Static Files](static_files.html)
|
||||||
|
|
|
@ -1,24 +1,31 @@
|
||||||
# Exceptions
|
# Exceptions
|
||||||
|
|
||||||
Exceptions can be thrown from within request handlers and will automatically be handled by Sanic. Exceptions take a message as their first argument, and can also take a status_code to be passed back in the HTTP response. Check sanic.exceptions for the full list of exceptions to throw.
|
Exceptions can be thrown from within request handlers and will automatically be
|
||||||
|
handled by Sanic. Exceptions take a message as their first argument, and can
|
||||||
|
also take a status code to be passed back in the HTTP response.
|
||||||
|
|
||||||
## Throwing an exception
|
## Throwing an exception
|
||||||
|
|
||||||
|
To throw an exception, simply `raise` the relevant exception from the
|
||||||
|
`sanic.exceptions` module.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
|
|
||||||
@app.route('/killme')
|
@app.route('/killme')
|
||||||
def i_am_ready_to_die(request):
|
def i_am_ready_to_die(request):
|
||||||
raise ServerError("Something bad happened")
|
raise ServerError("Something bad happened", status_code=500)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Handling Exceptions
|
## Handling exceptions
|
||||||
|
|
||||||
Just use the @exception decorator. The decorator expects a list of exceptions to handle as arguments. You can pass SanicException to catch them all! The exception handler must expect a request and exception object as arguments.
|
To override Sanic's default handling of an exception, the `@app.exception`
|
||||||
|
decorator is used. The decorator expects a list of exceptions to handle as
|
||||||
|
arguments. You can pass `SanicException` to catch them all! The decorated
|
||||||
|
exception handler function must take a `Request` and `Exception` object as
|
||||||
|
arguments.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import NotFound
|
from sanic.exceptions import NotFound
|
||||||
|
|
||||||
|
@ -26,3 +33,17 @@ from sanic.exceptions import NotFound
|
||||||
def ignore_404s(request, exception):
|
def ignore_404s(request, exception):
|
||||||
return text("Yep, I totally found the page: {}".format(request.url))
|
return text("Yep, I totally found the page: {}".format(request.url))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Useful exceptions
|
||||||
|
|
||||||
|
Some of the most useful exceptions are presented below:
|
||||||
|
|
||||||
|
- `NotFound`: called when a suitable route for the request isn't found.
|
||||||
|
- `ServerError`: called when something goes wrong inside the server. This
|
||||||
|
usually occurs if there is an exception raised in user code.
|
||||||
|
|
||||||
|
See the `sanic.exceptions` module for the full list of exceptions to throw.
|
||||||
|
|
||||||
|
**Previous:** [Middleware](middleware.html)
|
||||||
|
|
||||||
|
**Next:** [Blueprints](blueprints.html)
|
||||||
|
|
|
@ -2,5 +2,10 @@
|
||||||
|
|
||||||
A list of Sanic extensions created by the community.
|
A list of Sanic extensions created by the community.
|
||||||
|
|
||||||
* [Sessions](https://github.com/subyraman/sanic_session) — Support for sessions. Allows using redis, memcache or an in memory store.
|
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
|
||||||
* [CORS](https://github.com/ashleysommer/sanic-cors) — A port of flask-cors.
|
Allows using redis, memcache or an in memory store.
|
||||||
|
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||||
|
|
||||||
|
**Previous:** [Testing](testing.html)
|
||||||
|
|
||||||
|
**Next:** [Contributing](contributing.html)
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
Make sure you have pip and python 3.5 before starting
|
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
||||||
|
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
||||||
|
syntax, so earlier versions of python won't work.
|
||||||
|
|
||||||
|
1. Install Sanic: `python3 -m pip install sanic`
|
||||||
|
2. Create a file called `main.py` with the following code:
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
* Install Sanic
|
|
||||||
* `python3 -m pip install sanic`
|
|
||||||
* Edit main.py to include:
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import text
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({ "hello": "world" })
|
return text('Hello world!')
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
```
|
```
|
||||||
* Run `python3 main.py`
|
|
||||||
|
|
||||||
You now have a working Sanic server! To continue on, check out:
|
3. Run the server: `python3 main.py`
|
||||||
* [Request Data](request_data.md)
|
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||||
* [Routing](routing.md)
|
the message *Hello world!*.
|
||||||
|
|
||||||
|
You now have a working Sanic server!
|
||||||
|
|
||||||
|
**Next:** [Routing](routing.html)
|
||||||
|
|
|
@ -7,17 +7,17 @@ Guides
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
getting_started
|
getting_started
|
||||||
request_data
|
|
||||||
routing
|
routing
|
||||||
|
request_data
|
||||||
|
deploying
|
||||||
|
static_files
|
||||||
middleware
|
middleware
|
||||||
exceptions
|
exceptions
|
||||||
blueprints
|
blueprints
|
||||||
class_based_views
|
class_based_views
|
||||||
cookies
|
cookies
|
||||||
static_files
|
|
||||||
custom_protocol
|
custom_protocol
|
||||||
testing
|
testing
|
||||||
deploying
|
|
||||||
extensions
|
extensions
|
||||||
contributing
|
contributing
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,32 @@
|
||||||
# Middleware
|
# Middleware
|
||||||
|
|
||||||
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware returns a response object, the request will stop processing and a response will be returned.
|
Middleware are functions which are executed before or after requests to the
|
||||||
|
server. They can be used to modify the *request to* or *response from*
|
||||||
|
user-defined handler functions.
|
||||||
|
|
||||||
Middleware is registered via the middleware decorator, and can either be added as 'request' or 'response' middleware, based on the argument provided in the decorator. Response middleware receives both the request and the response as arguments.
|
There are two types of middleware: request and response. Both are declared
|
||||||
|
using the `@app.middleware` decorator, with the decorator's parameter being a
|
||||||
|
string representing its type: `'request'` or `'response'`. Response middleware
|
||||||
|
receives both the request and the response as arguments.
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
The simplest middleware doesn't modify the request or response at all:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
app = Sanic(__name__)
|
|
||||||
|
|
||||||
@app.middleware
|
|
||||||
async def halt_request(request):
|
|
||||||
print("I am a spy")
|
|
||||||
|
|
||||||
@app.middleware('request')
|
@app.middleware('request')
|
||||||
async def halt_request(request):
|
async def print_on_request(request):
|
||||||
return text('I halted the request')
|
print("I print when a request is received by the server")
|
||||||
|
|
||||||
@app.middleware('response')
|
@app.middleware('response')
|
||||||
async def halt_response(request, response):
|
async def print_on_response(request, response):
|
||||||
return text('I halted the response')
|
print("I print when a response is returned by the server")
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
async def handler(request):
|
|
||||||
return text('I would like to speak now please')
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Middleware chain
|
## Modifying the request or response
|
||||||
|
|
||||||
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware:
|
Middleware can modify the request or response parameter it is given, *as long
|
||||||
|
as it does not return it*. The following example shows a practical use-case for
|
||||||
|
this.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
@ -46,4 +42,29 @@ async def prevent_xss(request, response):
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
```
|
```
|
||||||
|
|
||||||
The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks.
|
The above code will apply the two middleware in order. First, the middleware
|
||||||
|
**custom_banner** will change the HTTP response header *Server* to
|
||||||
|
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
|
||||||
|
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
|
||||||
|
are invoked *after* a user function returns a response.
|
||||||
|
|
||||||
|
## Responding early
|
||||||
|
|
||||||
|
If middleware returns a `HTTPResponse` object, the request will stop processing
|
||||||
|
and the response will be returned. If this occurs to a request before the
|
||||||
|
relevant user route handler is reached, the handler will never be called.
|
||||||
|
Returning a response will also prevent any further middleware from running.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.middleware('request')
|
||||||
|
async def halt_request(request):
|
||||||
|
return text('I halted the request')
|
||||||
|
|
||||||
|
@app.middleware('response')
|
||||||
|
async def halt_response(request, response):
|
||||||
|
return text('I halted the response')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Previous:** [Static Files](static_files.html)
|
||||||
|
|
||||||
|
**Next:** [Exceptions](exceptions.html)
|
||||||
|
|
|
@ -1,31 +1,37 @@
|
||||||
# Request Data
|
# Request Data
|
||||||
|
|
||||||
## Properties
|
When an endpoint receives a HTTP request, the route function is passed a
|
||||||
|
`Request` object.
|
||||||
|
|
||||||
The following request variables are accessible as properties:
|
The following variables are accessible as properties on `Request` objects:
|
||||||
|
|
||||||
`request.files` (dictionary of File objects) - List of files that have a name, body, and type
|
- `json` (any) - JSON body
|
||||||
`request.json` (any) - JSON body
|
|
||||||
`request.args` (dict) - Query String variables. Use getlist to get multiple of the same name
|
|
||||||
`request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name
|
|
||||||
`request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type
|
|
||||||
`request.ip` (str) - IP address of the requester
|
|
||||||
|
|
||||||
See request.py for more information
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic.response import json
|
||||||
from sanic.response import json, text
|
|
||||||
|
|
||||||
@app.route("/json")
|
@app.route("/json")
|
||||||
def post_json(request):
|
def post_json(request):
|
||||||
return json({ "received": True, "message": request.json })
|
return json({ "received": True, "message": request.json })
|
||||||
|
```
|
||||||
|
|
||||||
@app.route("/form")
|
- `args` (dict) - Query string variables. A query string is the section of a
|
||||||
def post_json(request):
|
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||||
|
The request's `query_string` variable holds the unparsed string value.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/query_string")
|
||||||
|
def query_string(request):
|
||||||
|
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||||
|
```
|
||||||
|
|
||||||
|
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
@app.route("/files")
|
@app.route("/files")
|
||||||
def post_json(request):
|
def post_json(request):
|
||||||
|
@ -38,13 +44,52 @@ def post_json(request):
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
return json({ "received": True, "file_names": request.files.keys(), "test_file_parameters": file_parameters })
|
||||||
|
```
|
||||||
|
|
||||||
@app.route("/query_string")
|
- `form` (dict) - Posted form variables.
|
||||||
def query_string(request):
|
|
||||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/form")
|
||||||
|
def post_json(request):
|
||||||
|
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
||||||
|
```
|
||||||
|
|
||||||
|
- `body` (bytes) - Posted raw body. This property allows retrieval of the
|
||||||
|
request's raw data, regardless of content type.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route("/users", methods=["POST",])
|
@app.route("/users", methods=["POST",])
|
||||||
def create_user(request):
|
def create_user(request):
|
||||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
return text("You are trying to create a user with the following POST: %s" % request.body)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `ip` (str) - IP address of the requester.
|
||||||
|
|
||||||
|
## Accessing values using `get` and `getlist`
|
||||||
|
|
||||||
|
The request properties which return a dictionary actually return a subclass of
|
||||||
|
`dict` called `RequestParameters`. The key difference when using this object is
|
||||||
|
the distinction between the `get` and `getlist` methods.
|
||||||
|
|
||||||
|
- `get(key, default=None)` operates as normal, except that when the value of
|
||||||
|
the given key is a list, *only the first item is returned*.
|
||||||
|
- `getlist(key, default=None)` operates as normal, *returning the entire list*.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.request import RequestParameters
|
||||||
|
|
||||||
|
args = RequestParameters()
|
||||||
|
args['titles'] = ['Post 1', 'Post 2']
|
||||||
|
|
||||||
|
args.get('titles') # => 'Post 1'
|
||||||
|
|
||||||
|
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||||
|
```
|
||||||
|
|
||||||
|
**Previous:** [Routing](routing.html)
|
||||||
|
|
||||||
|
**Next:** [Deploying](deploying.html)
|
||||||
|
|
|
@ -1,17 +1,48 @@
|
||||||
# Routing
|
# Routing
|
||||||
|
|
||||||
Sanic comes with a basic router that supports request parameters. To specify a parameter, surround it with carrots like so: `<PARAM>`. Request parameters will be passed to the request handler functions as keyword arguments. To specify a type, add a :type after the parameter name, in the carrots. If the parameter does not match the type supplied, Sanic will throw a NotFound exception, resulting in a 404 page not found error.
|
Routing allows the user to specify handler functions for different URL endpoints.
|
||||||
|
|
||||||
|
A basic route looks like the following, where `app` is an instance of the
|
||||||
## Examples
|
`Sanic` class:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
return json({ "hello": "world" })
|
||||||
|
```
|
||||||
|
|
||||||
|
When the url `http://server.url/` is accessed (the base url of the server), the
|
||||||
|
final `/` is matched by the router to the handler function, `test`, which then
|
||||||
|
returns a JSON object.
|
||||||
|
|
||||||
|
Sanic handler functions must be defined using the `async def` syntax, as they
|
||||||
|
are asynchronous functions.
|
||||||
|
|
||||||
|
## Request parameters
|
||||||
|
|
||||||
|
Sanic comes with a basic router that supports request parameters.
|
||||||
|
|
||||||
|
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
|
||||||
|
Request parameters will be passed to the route handler functions as keyword
|
||||||
|
arguments.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route('/tag/<tag>')
|
@app.route('/tag/<tag>')
|
||||||
async def tag_handler(request, tag):
|
async def tag_handler(request, tag):
|
||||||
return text('Tag - {}'.format(tag))
|
return text('Tag - {}'.format(tag))
|
||||||
|
```
|
||||||
|
|
||||||
|
To specify a type for the parameter, add a `:type` after the parameter name,
|
||||||
|
inside the quotes. If the parameter does not match the specified type, Sanic
|
||||||
|
will throw a `NotFound` exception, resulting in a `404: Page not found` error
|
||||||
|
on the URL.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
@app.route('/number/<integer_arg:int>')
|
@app.route('/number/<integer_arg:int>')
|
||||||
async def integer_handler(request, integer_arg):
|
async def integer_handler(request, integer_arg):
|
||||||
|
@ -29,16 +60,52 @@ async def person_handler(request, name):
|
||||||
async def folder_handler(request, folder_id):
|
async def folder_handler(request, folder_id):
|
||||||
return text('Folder - {}'.format(folder_id))
|
return text('Folder - {}'.format(folder_id))
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP request types
|
||||||
|
|
||||||
|
By default, a route defined on a URL will be used for all requests to that URL.
|
||||||
|
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||||
|
which restricts the handler function to the HTTP methods in the given list.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.route('/post')
|
||||||
|
async def post_handler(request, methods=['POST']):
|
||||||
|
return text('POST request - {}'.format(request.json))
|
||||||
|
|
||||||
|
@app.route('/get')
|
||||||
|
async def GET_handler(request, methods=['GET']):
|
||||||
|
return text('GET request - {}'.format(request.args))
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `add_route` method
|
||||||
|
|
||||||
|
As we have seen, routes are often specified using the `@app.route` decorator.
|
||||||
|
However, this decorator is really just a wrapper for the `app.add_route`
|
||||||
|
method, which is used as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
# Define the handler functions
|
||||||
async def handler1(request):
|
async def handler1(request):
|
||||||
return text('OK')
|
return text('OK')
|
||||||
app.add_route(handler1, '/test')
|
|
||||||
|
|
||||||
async def handler2(request, name):
|
async def handler2(request, name):
|
||||||
return text('Folder - {}'.format(name))
|
return text('Folder - {}'.format(name))
|
||||||
app.add_route(handler2, '/folder/<name>')
|
|
||||||
|
|
||||||
async def person_handler2(request, name):
|
async def person_handler2(request, name):
|
||||||
return text('Person - {}'.format(name))
|
return text('Person - {}'.format(name))
|
||||||
app.add_route(person_handler2, '/person/<name:[A-z]>')
|
|
||||||
|
|
||||||
|
# Add each handler function as a route
|
||||||
|
app.add_route(handler1, '/test')
|
||||||
|
app.add_route(handler2, '/folder/<name>')
|
||||||
|
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Previous:** [Getting Started](getting_started.html)
|
||||||
|
|
||||||
|
**Next:** [Request Data](request_data.html)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
# Static Files
|
# Static Files
|
||||||
|
|
||||||
Both directories and files can be served by registering with static
|
Static files and directories, such as an image file, are served by Sanic when
|
||||||
|
registered with the `app.static` method. The method takes an endpoint URL and a
|
||||||
## Example
|
filename. The file specified will then be accessible via the given endpoint.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
# Serves files from the static folder to the URL /static
|
# Serves files from the static folder to the URL /static
|
||||||
|
@ -16,3 +17,7 @@ app.static('/the_best.png', '/home/ubuntu/test.png')
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Previous:** [Deploying](deploying.html)
|
||||||
|
|
||||||
|
**Next:** [Middleware](middleware.html)
|
||||||
|
|
|
@ -49,3 +49,7 @@ def test_endpoint_challenge():
|
||||||
# Assert that the server responds with the challenge string
|
# Assert that the server responds with the challenge string
|
||||||
assert response.text == request_data['challenge']
|
assert response.text == request_data['challenge']
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Previous:** [Custom protocols](custom_protocol.html)
|
||||||
|
|
||||||
|
**Next:** [Sanic extensions](extensions.html)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user