Merge pull request #2 from channelcat/master

update to latest sanic
This commit is contained in:
Ashley Sommer 2017-04-04 13:39:21 +10:00 committed by GitHub
commit ff1e88dde6
50 changed files with 1184 additions and 259 deletions

6
Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM python:3.6
ADD . /app
WORKDIR /app
RUN pip install tox

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
test:
find . -name "*.pyc" -delete
docker build -t sanic/test-image .
docker run -t sanic/test-image tox

View File

@ -59,6 +59,13 @@ Installation
- ``python -m pip install sanic`` - ``python -m pip install sanic``
To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
installation.
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic``
Documentation Documentation
------------- -------------

View File

@ -22,7 +22,7 @@ import sanic
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
extensions = [] extensions = ['sphinx.ext.autodoc']
templates_path = ['_templates'] templates_path = ['_templates']
@ -68,7 +68,6 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False todo_include_todos = False
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
@ -80,13 +79,11 @@ html_theme = 'sphinx_rtd_theme'
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------ # -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'Sanicdoc' htmlhelp_basename = 'Sanicdoc'
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
latex_elements = { latex_elements = {
@ -110,21 +107,14 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
(master_doc, 'Sanic.tex', 'Sanic Documentation', 'Sanic contributors', 'manual'), ]
'Sanic contributors', 'manual'),
]
# -- Options for manual page output --------------------------------------- # -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
(master_doc, 'sanic', 'Sanic Documentation',
[author], 1)
]
# -- Options for Texinfo output ------------------------------------------- # -- Options for Texinfo output -------------------------------------------
@ -132,13 +122,10 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'Sanic', 'Sanic Documentation', (master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
author, 'Sanic', 'One line description of project.', 'One line description of project.', 'Miscellaneous'),
'Miscellaneous'),
] ]
# -- Options for Epub output ---------------------------------------------- # -- Options for Epub output ----------------------------------------------
# Bibliographic Dublin Core info. # Bibliographic Dublin Core info.
@ -150,8 +137,6 @@ epub_copyright = copyright
# A list of files that should not be packed into the epub file. # A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html'] epub_exclude_files = ['search.html']
# -- Custom Settings ------------------------------------------------------- # -- Custom Settings -------------------------------------------------------
suppress_warnings = ['image.nonlocal_uri'] suppress_warnings = ['image.nonlocal_uri']

View File

@ -16,6 +16,7 @@ Guides
sanic/blueprints sanic/blueprints
sanic/config sanic/config
sanic/cookies sanic/cookies
sanic/streaming
sanic/class_based_views sanic/class_based_views
sanic/custom_protocol sanic/custom_protocol
sanic/ssl sanic/ssl

View File

@ -57,7 +57,7 @@ Blueprints have much the same functionality as an application instance.
### WebSocket routes ### WebSocket routes
WebSocket handlers can be registered on a blueprint using the `@bp.route` WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method. decorator or `bp.add_websocket_route` method.
### Middleware ### Middleware
@ -66,7 +66,7 @@ Using blueprints allows you to also register middleware globally.
```python ```python
@bp.middleware @bp.middleware
async def halt_request(request): async def print_on_request(request):
print("I am a spy") print("I am a spy")
@bp.middleware('request') @bp.middleware('request')

View File

@ -48,6 +48,24 @@ app.add_route(SimpleView.as_view(), '/')
``` ```
You can also use `async` syntax.
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleAsyncView(HTTPMethodView):
async def get(self, request):
return text('I am async get method')
app.add_route(SimpleAsyncView.as_view(), '/')
```
## URL parameters ## URL parameters
If you need any URL parameters, as discussed in the routing guide, include them If you need any URL parameters, as discussed in the routing guide, include them

View File

@ -29,6 +29,14 @@ In general the convention is to only have UPPERCASE configuration parameters. Th
There are several ways how to load configuration. There are several ways how to load configuration.
### From environment variables.
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_vars` boolean to the Sanic constructor to override that:
```python
app = Sanic(load_vars=False)
```
### From an Object ### From an Object
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module: If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
@ -75,4 +83,3 @@ Out of the box there are just a few predefined values which can be overwritten w
| ----------------- | --------- | --------------------------------- | | ----------------- | --------- | --------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) | | REQUEST_TIMEOUT | 60 | How long a request can take (sec) |

View File

@ -5,7 +5,7 @@ both read and write cookies, which are stored as key-value pairs.
## Reading cookies ## Reading cookies
A user's cookies can be accessed `Request` object's `cookie` dictionary. A user's cookies can be accessed via the `Request` object's `cookies` dictionary.
```python ```python
from sanic.response import text from sanic.response import text

39
docs/sanic/decorators.md Normal file
View File

@ -0,0 +1,39 @@
# Handler Decorators
Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
## Authorization Decorator
Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
```python
from functools import wraps
from sanic.response import json
def authorized():
def decorator(f):
@wraps(f)
async def decorated_function(request, *args, **kwargs):
# run some method that checks the request
# for the client's authorization status
is_authorized = check_request_for_authorization_status(request)
if is_authorized:
# the user is authorized.
# run the handler method and return the response
response = await f(request, *args, **kwargs)
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return decorated_function
return decorator
@app.route("/")
@authorized()
async def test(request):
return json({status: 'authorized'})
```

View File

@ -44,3 +44,15 @@ directly run by the interpreter.
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)
``` ```
## Running via Gunicorn
[Gunicorn](http://gunicorn.org/) Green Unicorn is a WSGI HTTP Server for UNIX.
Its a pre-fork worker model ported from Rubys Unicorn project.
In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker`
for Gunicorn `worker-class` argument:
```
gunicorn --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
```

View File

@ -16,3 +16,7 @@ A list of Sanic extensions created by the community.
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config. - [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the - [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`Babel` library `Babel` library
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic

View File

@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome! Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
Sanic aspires to be simple: Sanic aspires to be simple
------------------- ---------------------------
.. code:: python .. code:: python

View File

@ -1,9 +1,13 @@
# Middleware # Middleware And Listeners
Middleware are functions which are executed before or after requests to the 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* server. They can be used to modify the *request to* or *response from*
user-defined handler functions. user-defined handler functions.
Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle.
## Middleware
There are two types of middleware: request and response. Both are declared There are two types of middleware: request and response. Both are declared
using the `@app.middleware` decorator, with the decorator's parameter being a using the `@app.middleware` decorator, with the decorator's parameter being a
string representing its type: `'request'` or `'response'`. Response middleware string representing its type: `'request'` or `'response'`. Response middleware
@ -64,3 +68,45 @@ async def halt_request(request):
async def halt_response(request, response): async def halt_response(request, response):
return text('I halted the response') return text('I halted the response')
``` ```
## Listeners
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
- `before_server_start`
- `after_server_start`
- `before_server_stop`
- `after_server_stop`
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
For example:
```python
@app.listener('before_server_start')
async def setup_db(app, loop):
app.db = await db_setup()
@app.listener('after_server_start')
async def notify_server_started(app, loop):
print('Server successfully started!')
@app.listener('before_server_stop')
async def notify_server_stopping(app, loop):
print('Server shutting down!')
@app.listener('after_server_stop')
async def close_db(app, loop):
await app.db.close()
```
If you want to schedule a background task to run after the loop has started,
Sanic provides the `add_task` method to easily do so.
```python
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds())
```

View File

@ -17,7 +17,7 @@ The following variables are accessible as properties on `Request` objects:
- `args` (dict) - Query string variables. A query string is the section of a - `args` (dict) - Query string variables. A query string is the section of a
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed, URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`. the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
The request's `query_string` variable holds the unparsed string value. The request's `query_string` variable holds the unparsed string value.
```python ```python
@ -28,6 +28,10 @@ The following variables are accessible as properties on `Request` objects:
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
``` ```
- `raw_args` (dict) - On many cases you would need to access the url arguments in
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type - `files` (dictionary of `File` objects) - List of files that have a name, body, and type
```python ```python

View File

@ -2,7 +2,7 @@
Use functions in `sanic.response` module to create responses. Use functions in `sanic.response` module to create responses.
- `text` - Plain text response ## Plain Text
```python ```python
from sanic import response from sanic import response
@ -13,7 +13,7 @@ def handle_request(request):
return response.text('Hello world!') return response.text('Hello world!')
``` ```
- `html` - HTML response ## HTML
```python ```python
from sanic import response from sanic import response
@ -24,7 +24,7 @@ def handle_request(request):
return response.html('<p>Hello world!</p>') return response.html('<p>Hello world!</p>')
``` ```
- `json` - JSON response ## JSON
```python ```python
@ -36,7 +36,7 @@ def handle_request(request):
return response.json({'message': 'Hello world!'}) return response.json({'message': 'Hello world!'})
``` ```
- `file` - File response ## File
```python ```python
from sanic import response from sanic import response
@ -47,7 +47,7 @@ async def handle_request(request):
return await response.file('/srv/www/whatever.png') return await response.file('/srv/www/whatever.png')
``` ```
- `stream` - Streaming response ## Streaming
```python ```python
from sanic import response from sanic import response
@ -55,12 +55,12 @@ from sanic import response
@app.route("/streaming") @app.route("/streaming")
async def index(request): async def index(request):
async def streaming_fn(response): async def streaming_fn(response):
await response.write('foo') response.write('foo')
await response.write('bar') response.write('bar')
return response.stream(streaming_fn, content_type='text/plain') return response.stream(streaming_fn, content_type='text/plain')
``` ```
- `redirect` - Redirect response ## Redirect
```python ```python
from sanic import response from sanic import response
@ -71,7 +71,9 @@ def handle_request(request):
return response.redirect('/json') return response.redirect('/json')
``` ```
- `raw` - Raw response, response without encoding the body ## Raw
Response without encoding the body
```python ```python
from sanic import response from sanic import response
@ -82,6 +84,7 @@ def handle_request(request):
return response.raw('raw data') return response.raw('raw data')
``` ```
## Modify headers or status
To modify headers or status code, pass the `headers` or `status` argument to those functions: To modify headers or status code, pass the `headers` or `status` argument to those functions:

View File

@ -15,4 +15,5 @@ dependencies:
- httptools>=0.0.9 - httptools>=0.0.9
- ujson>=1.35 - ujson>=1.35
- aiofiles>=0.3.0 - aiofiles>=0.3.0
- websockets>=3.2
- https://github.com/channelcat/docutils-fork/zipball/master - https://github.com/channelcat/docutils-fork/zipball/master

View File

View File

@ -0,0 +1,140 @@
from sanic import Sanic
from sanic.exceptions import NotFound
from sanic.response import json
from sanic.views import HTTPMethodView
from asyncorm import configure_orm
from asyncorm.exceptions import QuerysetError
from library.models import Book
from library.serializer import BookSerializer
app = Sanic(name=__name__)
@app.listener('before_server_start')
def orm_configure(sanic, loop):
db_config = {'database': 'sanic_example',
'host': 'localhost',
'user': 'sanicdbuser',
'password': 'sanicDbPass',
}
# configure_orm needs a dictionary with:
# * the database configuration
# * the application/s where the models are defined
orm_app = configure_orm({'loop': loop, # always use the sanic loop!
'db_config': db_config,
'modules': ['library', ], # list of apps
})
# orm_app is the object that orchestrates the whole ORM
# sync_db should be run only once, better do that as external command
# it creates the tables in the database!!!!
# orm_app.sync_db()
# for all the 404 lets handle the exceptions
@app.exception(NotFound)
def ignore_404s(request, exception):
return json({'method': request.method,
'status': exception.status_code,
'error': exception.args[0],
'results': None,
})
# now the propper sanic workflow
class BooksView(HTTPMethodView):
def arg_parser(self, request):
parsed_args = {}
for k, v in request.args.items():
parsed_args[k] = v[0]
return parsed_args
async def get(self, request):
filtered_by = self.arg_parser(request)
if filtered_by:
q_books = await Book.objects.filter(**filtered_by)
else:
q_books = await Book.objects.all()
books = [BookSerializer.serialize(book) for book in q_books]
return json({'method': request.method,
'status': 200,
'results': books or None,
'count': len(books),
})
async def post(self, request):
# populate the book with the data in the request
book = Book(**request.json)
# and await on save
await book.save()
return json({'method': request.method,
'status': 201,
'results': BookSerializer.serialize(book),
})
class BookView(HTTPMethodView):
async def get_object(self, request, book_id):
try:
# await on database consults
book = await Book.objects.get(**{'id': book_id})
except QuerysetError as e:
raise NotFound(e.args[0])
return book
async def get(self, request, book_id):
# await on database consults
book = await self.get_object(request, book_id)
return json({'method': request.method,
'status': 200,
'results': BookSerializer.serialize(book),
})
async def put(self, request, book_id):
# await on database consults
book = await self.get_object(request, book_id)
# await on save
await book.save(**request.json)
return json({'method': request.method,
'status': 200,
'results': BookSerializer.serialize(book),
})
async def patch(self, request, book_id):
# await on database consults
book = await self.get_object(request, book_id)
# await on save
await book.save(**request.json)
return json({'method': request.method,
'status': 200,
'results': BookSerializer.serialize(book),
})
async def delete(self, request, book_id):
# await on database consults
book = await self.get_object(request, book_id)
# await on its deletion
await book.delete()
return json({'method': request.method,
'status': 200,
'results': None
})
app.add_route(BooksView.as_view(), '/books/')
app.add_route(BookView.as_view(), '/books/<book_id:int>/')
if __name__ == '__main__':
app.run()

View File

View File

@ -0,0 +1,21 @@
from asyncorm.model import Model
from asyncorm.fields import CharField, IntegerField, DateField
BOOK_CHOICES = (
('hard cover', 'hard cover book'),
('paperback', 'paperback book')
)
# This is a simple model definition
class Book(Model):
name = CharField(max_length=50)
synopsis = CharField(max_length=255)
book_type = CharField(max_length=15, null=True, choices=BOOK_CHOICES)
pages = IntegerField(null=True)
date_created = DateField(auto_now=True)
class Meta():
ordering = ['name', ]
unique_together = ['name', 'synopsis']

View File

@ -0,0 +1,15 @@
from asyncorm.model import ModelSerializer, SerializerMethod
from library.models import Book
class BookSerializer(ModelSerializer):
book_type = SerializerMethod()
def get_book_type(self, instance):
return instance.book_type_display()
class Meta():
model = Book
fields = [
'id', 'name', 'synopsis', 'book_type', 'pages', 'date_created'
]

View File

@ -0,0 +1,2 @@
asyncorm==0.0.7
sanic==0.4.1

View File

@ -0,0 +1,136 @@
# This demo requires aioredis and environmental variables established in ENV_VARS
import json
import logging
import os
from datetime import datetime
import aioredis
import sanic
from sanic import Sanic
ENV_VARS = ["REDIS_HOST", "REDIS_PORT",
"REDIS_MINPOOL", "REDIS_MAXPOOL",
"REDIS_PASS", "APP_LOGFILE"]
app = Sanic(name=__name__)
logger = None
@app.middleware("request")
async def log_uri(request):
# Simple middleware to log the URI endpoint that was called
logger.info("URI called: {0}".format(request.url))
@app.listener('before_server_start')
async def before_server_start(app, loop):
logger.info("Starting redis pool")
app.redis_pool = await aioredis.create_pool(
(app.config.REDIS_HOST, int(app.config.REDIS_PORT)),
minsize=int(app.config.REDIS_MINPOOL),
maxsize=int(app.config.REDIS_MAXPOOL),
password=app.config.REDIS_PASS)
@app.listener('after_server_stop')
async def after_server_stop(app, loop):
logger.info("Closing redis pool")
app.redis_pool.close()
await app.redis_pool.wait_closed()
@app.middleware("request")
async def attach_db_connectors(request):
# Just put the db objects in the request for easier access
logger.info("Passing redis pool to request object")
request["redis"] = request.app.redis_pool
@app.route("/state/<user_id>", methods=["GET"])
async def access_state(request, user_id):
try:
# Check to see if the value is in cache, if so lets return that
with await request["redis"] as redis_conn:
state = await redis_conn.get(user_id, encoding="utf-8")
if state:
return sanic.response.json({"msg": "Success",
"status": 200,
"success": True,
"data": json.loads(state),
"finished_at": datetime.now().isoformat()})
# Then state object is not in redis
logger.critical("Unable to find user_data in cache.")
return sanic.response.HTTPResponse({"msg": "User state not found",
"success": False,
"status": 404,
"finished_at": datetime.now().isoformat()}, status=404)
except aioredis.ProtocolError:
logger.critical("Unable to connect to state cache")
return sanic.response.HTTPResponse({"msg": "Internal Server Error",
"status": 500,
"success": False,
"finished_at": datetime.now().isoformat()}, status=500)
@app.route("/state/<user_id>/push", methods=["POST"])
async def set_state(request, user_id):
try:
# Pull a connection from the pool
with await request["redis"] as redis_conn:
# Set the value in cache to your new value
await redis_conn.set(user_id, json.dumps(request.json), expire=1800)
logger.info("Successfully pushed state to cache")
return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache",
"success": True,
"status": 200,
"finished_at": datetime.now().isoformat()})
except aioredis.ProtocolError:
logger.critical("Unable to connect to state cache")
return sanic.response.HTTPResponse({"msg": "Internal Server Error",
"status": 500,
"success": False,
"finished_at": datetime.now().isoformat()}, status=500)
def configure():
# Setup environment variables
env_vars = [os.environ.get(v, None) for v in ENV_VARS]
if not all(env_vars):
# Send back environment variables that were not set
return False, ", ".join([ENV_VARS[i] for i, flag in env_vars if not flag])
else:
# Add all the env vars to our app config
app.config.update({k: v for k, v in zip(ENV_VARS, env_vars)})
setup_logging()
return True, None
def setup_logging():
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
logging_format += "%(message)s"
logging.basicConfig(
filename=app.config.APP_LOGFILE,
format=logging_format,
level=logging.DEBUG)
def main(result, missing):
if result:
try:
app.run(host="0.0.0.0", port=8080, debug=True)
except:
logging.critical("User killed server. Closing")
else:
logging.critical("Unable to start. Missing environment variables [{0}]".format(missing))
if __name__ == "__main__":
result, missing = configure()
logger = logging.getLogger()
main(result, missing)

View File

@ -0,0 +1,62 @@
# encoding: utf-8
"""
You need the aiomysql
"""
import os
import aiomysql
from sanic import Sanic
from sanic.response import json
database_name = os.environ['DATABASE_NAME']
database_host = os.environ['DATABASE_HOST']
database_user = os.environ['DATABASE_USER']
database_password = os.environ['DATABASE_PASSWORD']
app = Sanic()
@app.listener("before_server_start")
async def get_pool(app, loop):
"""
the first param is the global instance ,
so we can store our connection pool in it .
and it can be used by different request
:param args:
:param kwargs:
:return:
"""
app.pool = {
"aiomysql": await aiomysql.create_pool(host=database_host, user=database_user, password=database_password,
db=database_name,
maxsize=5)}
async with app.pool['aiomysql'].acquire() as conn:
async with conn.cursor() as cur:
await cur.execute('DROP TABLE IF EXISTS sanic_polls')
await cur.execute("""CREATE TABLE sanic_polls (
id serial primary key,
question varchar(50),
pub_date timestamp
);""")
for i in range(0, 100):
await cur.execute("""INSERT INTO sanic_polls
(id, question, pub_date) VALUES ({}, {}, now())
""".format(i, i))
@app.route("/")
async def test():
result = []
data = {}
async with app.pool['aiomysql'].acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT question, pub_date FROM sanic_polls")
async for row in cur:
result.append({"question": row[0], "pub_date": row[1]})
if result or len(result) > 0:
data['data'] = res
return json(data)
if __name__ == '__main__':
app.run(host="127.0.0.1", workers=4, port=12000)

View File

@ -0,0 +1,34 @@
""" To run this example you need additional aioredis package
"""
from sanic import Sanic, response
import aioredis
app = Sanic(__name__)
@app.route("/")
async def handle(request):
async with request.app.redis_pool.get() as redis:
await redis.set('test-my-key', 'value')
val = await redis.get('test-my-key')
return response.text(val.decode('utf-8'))
@app.listener('before_server_start')
async def before_server_start(app, loop):
app.redis_pool = await aioredis.create_pool(
('localhost', 6379),
minsize=5,
maxsize=10,
loop=loop
)
@app.listener('after_server_stop')
async def after_server_stop(app, loop):
app.redis_pool.close()
await app.redis_pool.wait_closed()
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@ -1,3 +1,4 @@
## You need the following additional packages for this example ## You need the following additional packages for this example
# aiopg # aiopg
# peewee_async # peewee_async
@ -10,8 +11,9 @@ from sanic.response import json
## peewee_async related imports ## peewee_async related imports
import peewee import peewee
from peewee_async import Manager, PostgresqlDatabase from peewee import Model, BaseModel
from peewee_async import Manager, PostgresqlDatabase, execute
from functools import partial
# we instantiate a custom loop so we can pass it to our db manager # we instantiate a custom loop so we can pass it to our db manager
## from peewee_async docs: ## from peewee_async docs:
@ -19,42 +21,77 @@ from peewee_async import Manager, PostgresqlDatabase
# with manager! Its all automatic. But you can run Manager.connect() or # with manager! Its all automatic. But you can run Manager.connect() or
# Manager.close() when you need it. # Manager.close() when you need it.
class AsyncManager(Manager):
"""Inherit the peewee_async manager with our own object
configuration
# let's create a simple key value store: database.allow_sync = False
class KeyValue(peewee.Model): """
key = peewee.CharField(max_length=40, unique=True)
text = peewee.TextField(default='') def __init__(self, _model_class, *args, **kwargs):
super(AsyncManager, self).__init__(*args, **kwargs)
self._model_class = _model_class
self.database.allow_sync = False
def _do_fill(self, method, *args, **kwargs):
_class_method = getattr(super(AsyncManager, self), method)
pf = partial(_class_method, self._model_class)
return pf(*args, **kwargs)
def new(self, *args, **kwargs):
return self._do_fill('create', *args, **kwargs)
def get(self, *args, **kwargs):
return self._do_fill('get', *args, **kwargs)
def execute(self, query):
return execute(query)
def _get_meta_db_class(db):
"""creating a declartive class model for db"""
class _BlockedMeta(BaseModel):
def __new__(cls, name, bases, attrs):
_instance = super(_BlockedMeta, cls).__new__(cls, name, bases, attrs)
_instance.objects = AsyncManager(_instance, db)
return _instance
class _Base(Model, metaclass=_BlockedMeta):
def to_dict(self):
return self._data
class Meta: class Meta:
database = database database=db
return _Base
# create table synchronously
KeyValue.create_table(True)
# OPTIONAL: close synchronous connection
database.close()
# OPTIONAL: disable any future syncronous calls
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
app = Sanic('peewee_example') def declarative_base(*args, **kwargs):
"""Returns a new Modeled Class after inheriting meta and Model classes"""
db = PostgresqlDatabase(*args, **kwargs)
return _get_meta_db_class(db)
@app.listener('before_server_start')
def setup(app, loop): AsyncBaseModel = declarative_base(database='test',
database = PostgresqlDatabase(database='test',
host='127.0.0.1', host='127.0.0.1',
user='postgres', user='postgres',
password='mysecretpassword') password='mysecretpassword')
objects = Manager(database, loop=loop) # let's create a simple key value store:
class KeyValue(AsyncBaseModel):
key = peewee.CharField(max_length=40, unique=True)
text = peewee.TextField(default='')
app = Sanic('peewee_example')
@app.route('/post/<key>/<value>') @app.route('/post/<key>/<value>')
async def post(request, key, value): async def post(request, key, value):
""" """
Save get parameters to database Save get parameters to database
""" """
obj = await objects.create(KeyValue, key=key, text=value) obj = await KeyValue.objects.new(key=key, text=value)
return json({'object_id': obj.id}) return json({'object_id': obj.id})
@ -63,7 +100,7 @@ async def get(request):
""" """
Load all objects from database Load all objects from database
""" """
all_objects = await objects.execute(KeyValue.select()) all_objects = await KeyValue.objects.execute(KeyValue.select())
serialized_obj = [] serialized_obj = []
for obj in all_objects: for obj in all_objects:
serialized_obj.append({ serialized_obj.append({

View File

@ -9,4 +9,5 @@ async def test(request):
return json({"test": True}) return json({"test": True})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)

View File

@ -70,6 +70,11 @@ def query_string(request):
# Run Server # Run Server
# ----------------------------------------------- # # ----------------------------------------------- #
@app.listener('before_server_start')
def before_start(app, loop):
log.info("SERVER STARTING")
@app.listener('after_server_start') @app.listener('after_server_start')
def after_start(app, loop): def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH") log.info("OH OH OH OH OHHHHHHHH")
@ -77,7 +82,13 @@ def after_start(app, loop):
@app.listener('before_server_stop') @app.listener('before_server_stop')
def before_stop(app, loop): def before_stop(app, loop):
log.info("SERVER STOPPING")
@app.listener('after_server_stop')
def after_stop(app, loop):
log.info("TRIED EVERYTHING") log.info("TRIED EVERYTHING")
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@ -1,18 +1,10 @@
aiocache
aiofiles aiofiles
aiohttp aiohttp==1.3.5
beautifulsoup4 beautifulsoup4
bottle
coverage coverage
falcon
gunicorn
httptools httptools
kyoukai flake8
pytest pytest
recommonmark
sphinx
sphinx_rtd_theme
tornado
tox tox
ujson ujson
uvloop uvloop

View File

@ -1,7 +1,7 @@
import logging import logging
import re import re
import warnings import warnings
from asyncio import get_event_loop from asyncio import get_event_loop, ensure_future, CancelledError
from collections import deque, defaultdict from collections import deque, defaultdict
from functools import partial from functools import partial
from inspect import isawaitable, stack, getmodulename from inspect import isawaitable, stack, getmodulename
@ -25,7 +25,8 @@ from sanic.websocket import WebSocketProtocol, ConnectionClosed
class Sanic: class Sanic:
def __init__(self, name=None, router=None, error_handler=None): def __init__(self, name=None, router=None, error_handler=None,
load_env=True):
# Only set up a default log handler if the # Only set up a default log handler if the
# end-user application didn't set anything up. # end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET: if not logging.root.handlers and log.level == logging.NOTSET:
@ -44,7 +45,7 @@ class Sanic:
self.name = name self.name = name
self.router = router or Router() self.router = router or Router()
self.error_handler = error_handler or ErrorHandler() self.error_handler = error_handler or ErrorHandler()
self.config = Config() self.config = Config(load_env=load_env)
self.request_middleware = deque() self.request_middleware = deque()
self.response_middleware = deque() self.response_middleware = deque()
self.blueprints = {} self.blueprints = {}
@ -54,6 +55,7 @@ class Sanic:
self.listeners = defaultdict(list) self.listeners = defaultdict(list)
self.is_running = False self.is_running = False
self.websocket_enabled = False self.websocket_enabled = False
self.websocket_tasks = []
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@ -101,7 +103,8 @@ class Sanic:
return decorator return decorator
# Decorator # Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None): def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
"""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
@ -117,34 +120,42 @@ class Sanic:
def response(handler): def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler, self.router.add(uri=uri, methods=methods, handler=handler,
host=host) host=host, strict_slashes=strict_slashes)
return handler return handler
return response return response
# Shorthand method decorators # Shorthand method decorators
def get(self, uri, host=None): def get(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"GET"}), host=host) return self.route(uri, methods=frozenset({"GET"}), host=host,
strict_slashes=strict_slashes)
def post(self, uri, host=None): def post(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"POST"}), host=host) return self.route(uri, methods=frozenset({"POST"}), host=host,
strict_slashes=strict_slashes)
def put(self, uri, host=None): def put(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"PUT"}), host=host) return self.route(uri, methods=frozenset({"PUT"}), host=host,
strict_slashes=strict_slashes)
def head(self, uri, host=None): def head(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"HEAD"}), host=host) return self.route(uri, methods=frozenset({"HEAD"}), host=host,
strict_slashes=strict_slashes)
def options(self, uri, host=None): def options(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host) return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
strict_slashes=strict_slashes)
def patch(self, uri, host=None): def patch(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"PATCH"}), host=host) return self.route(uri, methods=frozenset({"PATCH"}), host=host,
strict_slashes=strict_slashes)
def delete(self, uri, host=None): def delete(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"DELETE"}), host=host) return self.route(uri, methods=frozenset({"DELETE"}), host=host,
strict_slashes=strict_slashes)
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):
"""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.
@ -168,17 +179,18 @@ class Sanic:
if isinstance(handler, CompositionView): if isinstance(handler, CompositionView):
methods = handler.handlers.keys() methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler) self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes)(handler)
return handler return handler
# Decorator # Decorator
def websocket(self, uri, host=None): def websocket(self, uri, host=None, strict_slashes=False):
"""Decorate a function to be registered as a websocket route """Decorate a function to be registered as a websocket route
:param uri: path of the URL :param uri: path of the URL
:param host: :param host:
:return: decorated function :return: decorated function
""" """
self.websocket_enabled = True self.enable_websocket()
# Fix case where the user did not prefix the URL with a / # Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working # and will probably get confused as to why it's not working
@ -190,22 +202,31 @@ class Sanic:
request.app = self request.app = self
protocol = request.transport.get_protocol() protocol = request.transport.get_protocol()
ws = await protocol.websocket_handshake(request) ws = await protocol.websocket_handshake(request)
# schedule the application handler
# its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.append(fut)
try: try:
# invoke the application handler await fut
await handler(request, ws, *args, **kwargs) except (CancelledError, ConnectionClosed):
except ConnectionClosed:
pass pass
self.websocket_tasks.remove(fut)
await ws.close() await ws.close()
self.router.add(uri=uri, handler=websocket_handler, self.router.add(uri=uri, handler=websocket_handler,
methods=frozenset({'GET'}), host=host) methods=frozenset({'GET'}), host=host,
strict_slashes=strict_slashes)
return handler return handler
return response return response
def add_websocket_route(self, handler, uri, host=None): def add_websocket_route(self, handler, uri, host=None,
strict_slashes=False):
"""A helper method to register a function as a websocket route.""" """A helper method to register a function as a websocket route."""
return self.websocket(uri, host=host)(handler) return self.websocket(uri, host=host,
strict_slashes=strict_slashes)(handler)
def enable_websocket(self, enable=True): def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket. """Enable or disable the support for websocket.
@ -213,6 +234,14 @@ class Sanic:
Websocket is enabled automatically if websocket routes are Websocket is enabled automatically if websocket routes are
added to the application. added to the application.
""" """
if not self.websocket_enabled:
# if the server is stopped, we want to cancel any ongoing
# websocket tasks, to allow the server to exit promptly
@self.listener('before_server_stop')
def cancel_websocket_tasks(app, loop):
for task in self.websocket_tasks:
task.cancel()
self.websocket_enabled = enable self.websocket_enabled = enable
def remove_route(self, uri, clean_cache=True, host=None): def remove_route(self, uri, clean_cache=True, host=None):
@ -305,7 +334,7 @@ class Sanic:
the output URL's query string. the output URL's query string.
:param view_name: string referencing the view name :param view_name: string referencing the view name
:param **kwargs: keys and values that are used to build request :param \*\*kwargs: keys and values that are used to build request
parameters and query string arguments. parameters and query string arguments.
:return: the built URL :return: the built URL
@ -526,19 +555,24 @@ class Sanic:
if protocol is None: if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol) else HttpProtocol)
if stop_event is not None:
if debug:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
server_settings = self._helper( server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start, host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop, after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
loop=loop, protocol=protocol, backlog=backlog, loop=loop, protocol=protocol, backlog=backlog,
stop_event=stop_event, register_sys_signals=register_sys_signals) register_sys_signals=register_sys_signals)
try: try:
self.is_running = True self.is_running = True
if workers == 1: if workers == 1:
serve(**server_settings) serve(**server_settings)
else: else:
serve_multiple(server_settings, workers, stop_event) serve_multiple(server_settings, workers)
except: except:
log.exception( log.exception(
'Experienced exception while trying to serve') 'Experienced exception while trying to serve')
@ -550,6 +584,10 @@ class Sanic:
"""This kills the Sanic""" """This kills the Sanic"""
get_event_loop().stop() get_event_loop().stop()
def __call__(self):
"""gunicorn compatibility"""
return self
async def create_server(self, host="127.0.0.1", port=8000, debug=False, async def create_server(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None, before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None, before_stop=None, after_stop=None, ssl=None,
@ -563,13 +601,17 @@ class Sanic:
if protocol is None: if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol) else HttpProtocol)
if stop_event is not None:
if debug:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
server_settings = self._helper( server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start, host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop, after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock, after_stop=after_stop, ssl=ssl, sock=sock,
loop=loop or get_event_loop(), protocol=protocol, loop=loop or get_event_loop(), protocol=protocol,
backlog=backlog, stop_event=stop_event, backlog=backlog, run_async=True)
run_async=True)
return await serve(**server_settings) return await serve(**server_settings)
@ -589,7 +631,11 @@ class Sanic:
context = create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) context = create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key) context.load_cert_chain(cert, keyfile=key)
ssl = context ssl = context
if stop_event is not None:
if debug:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
if loop is not None: if loop is not None:
if debug: if debug:
warnings.simplefilter('default') warnings.simplefilter('default')
@ -658,6 +704,7 @@ class Sanic:
server_settings['run_async'] = True server_settings['run_async'] = True
# Serve # Serve
if host and port:
proto = "http" proto = "http"
if ssl is not None: if ssl is not None:
proto = "https" proto = "https"

View File

@ -3,7 +3,9 @@ from collections import defaultdict, namedtuple
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView from sanic.views import CompositionView
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) FutureRoute = namedtuple('Route',
['handler', 'uri', 'methods',
'host', 'strict_slashes'])
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'])
@ -44,7 +46,8 @@ class Blueprint:
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
)(future.handler) )(future.handler)
for future in self.websocket_routes: for future in self.websocket_routes:
@ -55,7 +58,8 @@ class Blueprint:
uri = url_prefix + future.uri if url_prefix else future.uri uri = url_prefix + future.uri if url_prefix else future.uri
app.websocket( app.websocket(
uri=uri, uri=uri,
host=future.host or self.host host=future.host or self.host,
strict_slashes=future.strict_slashes
)(future.handler) )(future.handler)
# Middleware # Middleware
@ -82,19 +86,21 @@ class Blueprint:
for listener in listeners: for listener in listeners:
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):
"""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.
:param methods: list of acceptable HTTP methods. :param methods: list of acceptable HTTP methods.
""" """
def decorator(handler): def decorator(handler):
route = FutureRoute(handler, uri, methods, host) route = FutureRoute(handler, uri, methods, host, strict_slashes)
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):
"""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,
@ -115,16 +121,17 @@ class Blueprint:
if isinstance(handler, CompositionView): if isinstance(handler, CompositionView):
methods = handler.handlers.keys() methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler) self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes)(handler)
return handler return handler
def websocket(self, uri, host=None): def websocket(self, uri, host=None, strict_slashes=False):
"""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) route = FutureRoute(handler, uri, [], host, strict_slashes)
self.websocket_routes.append(route) self.websocket_routes.append(route)
return handler return handler
return decorator return decorator
@ -183,23 +190,30 @@ class Blueprint:
self.statics.append(static) self.statics.append(static)
# Shorthand method decorators # Shorthand method decorators
def get(self, uri, host=None): def get(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["GET"], host=host) return self.route(uri, methods=["GET"], host=host,
strict_slashes=strict_slashes)
def post(self, uri, host=None): def post(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["POST"], host=host) return self.route(uri, methods=["POST"], host=host,
strict_slashes=strict_slashes)
def put(self, uri, host=None): def put(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["PUT"], host=host) return self.route(uri, methods=["PUT"], host=host,
strict_slashes=strict_slashes)
def head(self, uri, host=None): def head(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["HEAD"], host=host) return self.route(uri, methods=["HEAD"], host=host,
strict_slashes=strict_slashes)
def options(self, uri, host=None): def options(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["OPTIONS"], host=host) return self.route(uri, methods=["OPTIONS"], host=host,
strict_slashes=strict_slashes)
def patch(self, uri, host=None): def patch(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["PATCH"], host=host) return self.route(uri, methods=["PATCH"], host=host,
strict_slashes=strict_slashes)
def delete(self, uri, host=None): def delete(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["DELETE"], host=host) return self.route(uri, methods=["DELETE"], host=host,
strict_slashes=strict_slashes)

View File

@ -1,9 +1,12 @@
import os import os
import types import types
SANIC_PREFIX = 'SANIC_'
class Config(dict): class Config(dict):
def __init__(self, defaults=None): def __init__(self, defaults=None, load_env=True):
super().__init__(defaults or {}) super().__init__(defaults or {})
self.LOGO = """ self.LOGO = """
@ -29,6 +32,9 @@ class Config(dict):
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
self.REQUEST_TIMEOUT = 60 # 60 seconds self.REQUEST_TIMEOUT = 60 # 60 seconds
if load_env:
self.load_environment_vars()
def __getattr__(self, attr): def __getattr__(self, attr):
try: try:
return self[attr] return self[attr]
@ -90,3 +96,13 @@ class Config(dict):
for key in dir(obj): for key in dir(obj):
if key.isupper(): if key.isupper():
self[key] = getattr(obj, key) self[key] = getattr(obj, key)
def load_environment_vars(self):
for k, v in os.environ.items():
"""
Looks for any SANIC_ prefixed environment variables and applies
them to the configuration if present.
"""
if k.startswith(SANIC_PREFIX):
_, config_key = k.split(SANIC_PREFIX, 1)
self[config_key] = v

View File

@ -19,7 +19,7 @@ _Translator.update({
def _quote(str): def _quote(str):
r"""Quote a string for use in a cookie header. """Quote a string for use in a cookie header.
If the string does not need to be double-quoted, then just return the If the string does not need to be double-quoted, then just return the
string. Otherwise, surround the string in doublequotes and quote string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters. (with a \) special characters.

View File

@ -121,6 +121,10 @@ class Request(dict):
self.parsed_args = RequestParameters() self.parsed_args = RequestParameters()
return self.parsed_args return self.parsed_args
@property
def raw_args(self):
return {k: v[0] for k, v in self.args.items()}
@property @property
def cookies(self): def cookies(self):
if self._cookies is None: if self._cookies is None:
@ -142,10 +146,16 @@ class Request(dict):
@property @property
def scheme(self): def scheme(self):
if self.transport.get_extra_info('sslcontext'): if self.app.websocket_enabled \
return 'https' and self.headers.get('upgrade') == 'websocket':
scheme = 'ws'
else:
scheme = 'http'
return 'http' if self.transport.get_extra_info('sslcontext'):
scheme += 's'
return scheme
@property @property
def host(self): def host(self):

View File

@ -1,5 +1,6 @@
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
try: try:
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
except: except:
@ -132,8 +133,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
async def stream( async def stream(
self, version="1.1", keep_alive=False, keep_alive_timeout=None): self, version="1.1", keep_alive=False, keep_alive_timeout=None):
"""Streams headers, runs the `streaming_fn` callback that writes content """Streams headers, runs the `streaming_fn` callback that writes
to the response body, then finalizes the response body. content to the response body, then finalizes the response body.
""" """
headers = self.get_headers( headers = self.get_headers(
version, keep_alive=keep_alive, version, keep_alive=keep_alive,
@ -251,8 +252,7 @@ def text(body, status=200, headers=None,
:param body: Response data to be encoded. :param body: Response data to be encoded.
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
:param content_type: :param content_type: the content type (string) of the response
the content type (string) of the response
""" """
return HTTPResponse( return HTTPResponse(
body, status=status, headers=headers, body, status=status, headers=headers,
@ -266,8 +266,7 @@ def raw(body, status=200, headers=None,
:param body: Response data. :param body: Response data.
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
:param content_type: :param content_type: the content type (string) of the response.
the content type (string) of the response
""" """
return HTTPResponse(body_bytes=body, status=status, headers=headers, return HTTPResponse(body_bytes=body, status=status, headers=headers,
content_type=content_type) content_type=content_type)
@ -316,9 +315,9 @@ def stream(
content_type="text/plain; charset=utf-8"): content_type="text/plain; charset=utf-8"):
"""Accepts an coroutine `streaming_fn` which can be used to """Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`. write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
Example usage:
``` Example usage::
@app.route("/") @app.route("/")
async def index(request): async def index(request):
async def streaming_fn(response): async def streaming_fn(response):
@ -326,7 +325,6 @@ def stream(
await response.write('bar') await response.write('bar')
return stream(streaming_fn, content_type='text/plain') return stream(streaming_fn, content_type='text/plain')
```
:param streaming_fn: A coroutine accepts a response and :param streaming_fn: A coroutine accepts a response and
writes content to that response. writes content to that response.
@ -334,7 +332,11 @@ def stream(
:param headers: Custom Headers. :param headers: Custom Headers.
""" """
return StreamingHTTPResponse( return StreamingHTTPResponse(
streaming_fn, headers=headers, content_type=content_type, status=200) streaming_fn,
headers=headers,
content_type=content_type,
status=status
)
def redirect(to, headers=None, status=302, def redirect(to, headers=None, status=302,

View File

@ -75,8 +75,9 @@ class Router:
"""Parse a parameter string into its constituent name, type, and """Parse a parameter string into its constituent name, type, and
pattern pattern
For example: For example::
`parse_parameter_string('<param_one:[A-z]>')` ->
parse_parameter_string('<param_one:[A-z]>')` ->
('param_one', str, '[A-z]') ('param_one', str, '[A-z]')
:param parameter_string: String to parse :param parameter_string: String to parse
@ -95,9 +96,15 @@ class Router:
return name, _type, pattern return name, _type, pattern
def add(self, uri, methods, handler, host=None): def add(self, uri, methods, handler, host=None, strict_slashes=False):
# add regular version # add regular version
self._add(uri, methods, handler, host) self._add(uri, methods, handler, host)
if strict_slashes:
return
# Add versions with and without trailing /
slash_is_missing = ( slash_is_missing = (
not uri[-1] == '/' not uri[-1] == '/'
and not self.routes_all.get(uri + '/', False) and not self.routes_all.get(uri + '/', False)

View File

@ -4,10 +4,13 @@ import traceback
import warnings import warnings
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
from multiprocessing import Process, Event from multiprocessing import Process
from os import set_inheritable from os import set_inheritable
from signal import SIGTERM, SIGINT from signal import (
from signal import signal as signal_func SIGTERM, SIGINT,
signal as signal_func,
Signals
)
from socket import socket, SOL_SOCKET, SO_REUSEADDR from socket import socket, SOL_SOCKET, SO_REUSEADDR
from time import time from time import time
@ -313,7 +316,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False, after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, ssl=None, sock=None, request_max_size=None, request_timeout=60, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False): register_sys_signals=True, run_async=False, connections=None,
signal=Signal()):
"""Start asynchronous HTTP Server on an individual process. """Start asynchronous HTTP Server on an individual process.
:param host: Address to host on :param host: Address to host on
@ -349,8 +353,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(before_start, loop) trigger_events(before_start, loop)
connections = set() connections = connections if connections is not None else set()
signal = Signal()
server = partial( server = partial(
protocol, protocol,
loop=loop, loop=loop,
@ -421,7 +424,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
loop.close() loop.close()
def serve_multiple(server_settings, workers, stop_event=None): def serve_multiple(server_settings, workers):
"""Start multiple server processes simultaneously. Stop on interrupt """Start multiple server processes simultaneously. Stop on interrupt
and terminate signals, and drain connections when complete. and terminate signals, and drain connections when complete.
@ -448,11 +451,12 @@ def serve_multiple(server_settings, workers, stop_event=None):
server_settings['host'] = None server_settings['host'] = None
server_settings['port'] = None server_settings['port'] = None
if stop_event is None: def sig_handler(signal, frame):
stop_event = Event() log.info("Received signal {}. Shutting down.".format(
Signals(signal).name))
signal_func(SIGINT, lambda s, f: stop_event.set()) signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: stop_event.set()) signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
processes = [] processes = []
for _ in range(workers): for _ in range(workers):

View File

@ -22,7 +22,10 @@ class SanicTestClient:
cookies=cookies, connector=conn) as session: cookies=cookies, connector=conn) as session:
async with getattr( async with getattr(
session, method.lower())(url, *args, **kwargs) as response: session, method.lower())(url, *args, **kwargs) as response:
try:
response.text = await response.text() response.text = await response.text()
except UnicodeDecodeError as e:
response.text = None
response.body = await response.read() response.body = await response.read()
return response return response

166
sanic/worker.py Normal file
View File

@ -0,0 +1,166 @@
import os
import sys
import signal
import asyncio
import logging
try:
import ssl
except ImportError:
ssl = None
import uvloop
import gunicorn.workers.base as base
from sanic.server import trigger_events, serve, HttpProtocol, Signal
from sanic.websocket import WebSocketProtocol
class GunicornWorker(base.Worker):
def __init__(self, *args, **kw): # pragma: no cover
super().__init__(*args, **kw)
cfg = self.cfg
if cfg.is_ssl:
self.ssl_context = self._create_ssl_context(cfg)
else:
self.ssl_context = None
self.servers = []
self.connections = set()
self.exit_code = 0
self.signal = Signal()
def init_process(self):
# create new event_loop after fork
asyncio.get_event_loop().close()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
super().init_process()
def run(self):
is_debug = self.log.loglevel == logging.DEBUG
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled
else HttpProtocol)
self._server_settings = self.app.callable._helper(
host=None,
port=None,
loop=self.loop,
debug=is_debug,
protocol=protocol,
ssl=self.ssl_context,
run_async=True
)
self._server_settings.pop('sock')
trigger_events(self._server_settings.get('before_start', []),
self.loop)
self._server_settings['before_start'] = ()
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
try:
self.loop.run_until_complete(self._runner)
self.app.callable.is_running = True
trigger_events(self._server_settings.get('after_start', []),
self.loop)
self.loop.run_until_complete(self._check_alive())
trigger_events(self._server_settings.get('before_stop', []),
self.loop)
self.loop.run_until_complete(self.close())
finally:
trigger_events(self._server_settings.get('after_stop', []),
self.loop)
self.loop.close()
sys.exit(self.exit_code)
async def close(self):
if self.servers:
# stop accepting connections
self.log.info("Stopping server: %s, connections: %s",
self.pid, len(self.connections))
for server in self.servers:
server.close()
await server.wait_closed()
self.servers.clear()
# prepare connections for closing
self.signal.stopped = True
for conn in self.connections:
conn.close_if_idle()
while self.connections:
await asyncio.sleep(0.1)
async def _run(self):
for sock in self.sockets:
self.servers.append(await serve(
sock=sock,
connections=self.connections,
signal=self.signal,
**self._server_settings
))
async def _check_alive(self):
# If our parent changed then we shut down.
pid = os.getpid()
try:
while self.alive:
self.notify()
if pid == os.getpid() and self.ppid != os.getppid():
self.alive = False
self.log.info("Parent changed, shutting down: %s", self)
else:
await asyncio.sleep(1.0, loop=self.loop)
except (Exception, BaseException, GeneratorExit, KeyboardInterrupt):
pass
@staticmethod
def _create_ssl_context(cfg):
""" Creates SSLContext instance for usage in asyncio.create_server.
See ssl.SSLSocket.__init__ for more details.
"""
ctx = ssl.SSLContext(cfg.ssl_version)
ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
ctx.verify_mode = cfg.cert_reqs
if cfg.ca_certs:
ctx.load_verify_locations(cfg.ca_certs)
if cfg.ciphers:
ctx.set_ciphers(cfg.ciphers)
return ctx
def init_signals(self):
# Set up signals through the event loop API.
self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
signal.SIGQUIT, None)
self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
signal.SIGTERM, None)
self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
signal.SIGINT, None)
self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
signal.SIGWINCH, None)
self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
signal.SIGUSR1, None)
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
signal.SIGABRT, None)
# Don't let SIGTERM and SIGUSR1 disturb active requests
# by interrupting system calls
signal.siginterrupt(signal.SIGTERM, False)
signal.siginterrupt(signal.SIGUSR1, False)
def handle_quit(self, sig, frame):
self.alive = False
self.cfg.worker_int(self)
def handle_abort(self, sig, frame):
self.alive = False
self.exit_code = 1
self.cfg.worker_abort(self)

View File

@ -4,8 +4,10 @@ Sanic
import codecs import codecs
import os import os
import re import re
from setuptools import setup from distutils.errors import DistutilsPlatformError
from distutils.util import strtobool
from setuptools import setup
with codecs.open(os.path.join(os.path.abspath(os.path.dirname( with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp: __file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
@ -35,23 +37,32 @@ setup_kwargs = {
], ],
} }
try: ujson = 'ujson>=1.35'
normal_requirements = [ uvloop = 'uvloop>=0.5.3'
requirements = [
'httptools>=0.0.9', 'httptools>=0.0.9',
'uvloop>=0.5.3', uvloop,
'ujson>=1.35', ujson,
'aiofiles>=0.3.0', 'aiofiles>=0.3.0',
'websockets>=3.2', 'websockets>=3.2',
] ]
setup_kwargs['install_requires'] = normal_requirements if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON")
requirements.remove(ujson)
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
print("Installing without uvLoop")
requirements.remove(uvloop)
try:
setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs) setup(**setup_kwargs)
except DistutilsPlatformError as exception: except DistutilsPlatformError as exception:
windows_requirements = [ requirements.remove(ujson)
'httptools>=0.0.9', requirements.remove(uvloop)
'aiofiles>=0.3.0', print("Installing without uJSON or uvLoop")
'websockets>=3.2', setup_kwargs['install_requires'] = requirements
]
setup_kwargs['install_requires'] = windows_requirements
setup(**setup_kwargs) setup(**setup_kwargs)
# Installation was successful # Installation was successful

BIN
tests/static/python.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -24,6 +24,33 @@ def test_bp():
assert response.text == 'Hello' assert response.text == 'Hello'
def test_bp_strict_slash():
app = Sanic('test_route_strict_slash')
bp = Blueprint('test_text')
@bp.get('/get', strict_slashes=True)
def handler(request):
return text('OK')
@bp.post('/post/', strict_slashes=True)
def handler(request):
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.get('/get/')
assert response.status == 404
request, response = app.test_client.post('/post/')
assert response.text == 'OK'
request, response = app.test_client.post('/post')
assert response.status == 404
def test_bp_with_url_prefix(): def test_bp_with_url_prefix():
app = Sanic('test_text') app = Sanic('test_text')
bp = Blueprint('test_text', url_prefix='/test1') bp = Blueprint('test_text', url_prefix='/test1')

View File

@ -16,6 +16,17 @@ def test_load_from_object():
assert app.config.CONFIG_VALUE == 'should be used' assert app.config.CONFIG_VALUE == 'should be used'
assert 'not_for_config' not in app.config assert 'not_for_config' not in app.config
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic()
assert app.config.TEST_ANSWER == "42"
del environ["SANIC_TEST_ANSWER"]
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(load_env=False)
assert getattr(app.config, 'TEST_ANSWER', None) == None
del environ["SANIC_TEST_ANSWER"]
def test_load_from_file(): def test_load_from_file():
app = Sanic('test_load_from_file') app = Sanic('test_load_from_file')

View File

@ -1,49 +1,47 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import text
from sanic.exceptions import PayloadTooLarge from sanic.exceptions import PayloadTooLarge
from sanic.response import text
def test_payload_too_large_from_error_handler():
data_received_app = Sanic('data_received') data_received_app = Sanic('data_received')
data_received_app.config.REQUEST_MAX_SIZE = 1 data_received_app.config.REQUEST_MAX_SIZE = 1
data_received_default_app = Sanic('data_received_default')
data_received_default_app.config.REQUEST_MAX_SIZE = 1
on_header_default_app = Sanic('on_header')
on_header_default_app.config.REQUEST_MAX_SIZE = 500
@data_received_app.route('/1') @data_received_app.route('/1')
async def handler1(request): async def handler1(request):
return text('OK') return text('OK')
@data_received_app.exception(PayloadTooLarge) @data_received_app.exception(PayloadTooLarge)
def handler_exception(request, exception): def handler_exception(request, exception):
return text('Payload Too Large from error_handler.', 413) return text('Payload Too Large from error_handler.', 413)
def test_payload_too_large_from_error_handler():
response = data_received_app.test_client.get('/1', gather_request=False) response = data_received_app.test_client.get('/1', gather_request=False)
assert response.status == 413 assert response.status == 413
assert response.text == 'Payload Too Large from error_handler.' assert response.text == 'Payload Too Large from error_handler.'
def test_payload_too_large_at_data_received_default():
data_received_default_app = Sanic('data_received_default')
data_received_default_app.config.REQUEST_MAX_SIZE = 1
@data_received_default_app.route('/1') @data_received_default_app.route('/1')
async def handler2(request): async def handler2(request):
return text('OK') return text('OK')
def test_payload_too_large_at_data_received_default():
response = data_received_default_app.test_client.get( response = data_received_default_app.test_client.get(
'/1', gather_request=False) '/1', gather_request=False)
assert response.status == 413 assert response.status == 413
assert response.text == 'Error: Payload Too Large' assert response.text == 'Error: Payload Too Large'
@on_header_default_app.route('/1') def test_payload_too_large_at_on_header_default():
on_header_default_app = Sanic('on_header')
on_header_default_app.config.REQUEST_MAX_SIZE = 500
@on_header_default_app.post('/1')
async def handler3(request): async def handler3(request):
return text('OK') return text('OK')
def test_payload_too_large_at_on_header_default():
data = 'a' * 1000 data = 'a' * 1000
response = on_header_default_app.test_client.post( response = on_header_default_app.test_client.post(
'/1', gather_request=False, data=data) '/1', gather_request=False, data=data)

View File

@ -88,4 +88,7 @@ def test_chained_redirect(redirect_app):
assert request.url.endswith('/1') assert request.url.endswith('/1')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
try:
assert response.url.endswith('/3') assert response.url.endswith('/3')
except AttributeError:
assert response.url.path.endswith('/3')

View File

@ -23,6 +23,29 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get') request, response = app.test_client.post('/get')
assert response.status == 405 assert response.status == 405
def test_route_strict_slash():
app = Sanic('test_route_strict_slash')
@app.get('/get', strict_slashes=True)
def handler(request):
return text('OK')
@app.post('/post/', strict_slashes=True)
def handler(request):
return text('OK')
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.get('/get/')
assert response.status == 404
request, response = app.test_client.post('/post/')
assert response.text == 'OK'
request, response = app.test_client.post('/post')
assert response.status == 404
def test_route_optional_slash(): def test_route_optional_slash():
app = Sanic('test_route_optional_slash') app = Sanic('test_route_optional_slash')

View File

@ -25,7 +25,7 @@ def get_file_content(static_file_directory, file_name):
return file.read() return file.read()
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name): def test_static_file(static_file_directory, file_name):
app = Sanic('test_static') app = Sanic('test_static')
app.static( app.static(

View File

@ -15,13 +15,13 @@ def test_methods(method):
class DummyView(HTTPMethodView): class DummyView(HTTPMethodView):
def get(self, request): async def get(self, request):
return text('', headers={'method': 'GET'}) return text('', headers={'method': 'GET'})
def post(self, request): def post(self, request):
return text('', headers={'method': 'POST'}) return text('', headers={'method': 'POST'})
def put(self, request): async def put(self, request):
return text('', headers={'method': 'PUT'}) return text('', headers={'method': 'PUT'})
def head(self, request): def head(self, request):
@ -30,7 +30,7 @@ def test_methods(method):
def options(self, request): def options(self, request):
return text('', headers={'method': 'OPTIONS'}) return text('', headers={'method': 'OPTIONS'})
def patch(self, request): async def patch(self, request):
return text('', headers={'method': 'PATCH'}) return text('', headers={'method': 'PATCH'})
def delete(self, request): def delete(self, request):

View File

@ -10,12 +10,7 @@ python =
[testenv] [testenv]
deps = deps =
aiofiles -rrequirements-dev.txt
aiohttp
websockets
pytest
beautifulsoup4
coverage
commands = commands =
pytest tests {posargs} pytest tests {posargs}