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``
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
-------------

View File

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

View File

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

View File

@ -57,7 +57,7 @@ Blueprints have much the same functionality as an application instance.
### 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.
### Middleware
@ -66,7 +66,7 @@ Using blueprints allows you to also register middleware globally.
```python
@bp.middleware
async def halt_request(request):
async def print_on_request(request):
print("I am a spy")
@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
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.
### 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
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_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
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
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__':
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.
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`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 aspires to be simple:
-------------------
Sanic aspires to be simple
---------------------------
.. code:: python

View File

@ -1,9 +1,13 @@
# Middleware
# Middleware And Listeners
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.
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
using the `@app.middleware` decorator, with the decorator's parameter being a
string representing its type: `'request'` or `'response'`. Response middleware
@ -64,3 +68,45 @@ async def halt_request(request):
async def halt_response(request, 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
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.
```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 })
```
- `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
```python

View File

@ -2,7 +2,7 @@
Use functions in `sanic.response` module to create responses.
- `text` - Plain text response
## Plain Text
```python
from sanic import response
@ -13,7 +13,7 @@ def handle_request(request):
return response.text('Hello world!')
```
- `html` - HTML response
## HTML
```python
from sanic import response
@ -24,7 +24,7 @@ def handle_request(request):
return response.html('<p>Hello world!</p>')
```
- `json` - JSON response
## JSON
```python
@ -36,7 +36,7 @@ def handle_request(request):
return response.json({'message': 'Hello world!'})
```
- `file` - File response
## File
```python
from sanic import response
@ -47,7 +47,7 @@ async def handle_request(request):
return await response.file('/srv/www/whatever.png')
```
- `stream` - Streaming response
## Streaming
```python
from sanic import response
@ -55,12 +55,12 @@ from sanic import response
@app.route("/streaming")
async def index(request):
async def streaming_fn(response):
await response.write('foo')
await response.write('bar')
response.write('foo')
response.write('bar')
return response.stream(streaming_fn, content_type='text/plain')
```
- `redirect` - Redirect response
## Redirect
```python
from sanic import response
@ -71,7 +71,9 @@ def handle_request(request):
return response.redirect('/json')
```
- `raw` - Raw response, response without encoding the body
## Raw
Response without encoding the body
```python
from sanic import response
@ -82,6 +84,7 @@ def handle_request(request):
return response.raw('raw data')
```
## Modify headers or status
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
- ujson>=1.35
- aiofiles>=0.3.0
- websockets>=3.2
- 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
# aiopg
# peewee_async
@ -10,8 +11,9 @@ from sanic.response import json
## peewee_async related imports
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
## 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
# 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:
class KeyValue(peewee.Model):
key = peewee.CharField(max_length=40, unique=True)
text = peewee.TextField(default='')
database.allow_sync = False
"""
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:
database = database
# 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
database=db
return _Base
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):
database = PostgresqlDatabase(database='test',
AsyncBaseModel = declarative_base(database='test',
host='127.0.0.1',
user='postgres',
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>')
async def post(request, key, value):
"""
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})
@ -63,7 +100,7 @@ async def get(request):
"""
Load all objects from database
"""
all_objects = await objects.execute(KeyValue.select())
all_objects = await KeyValue.objects.execute(KeyValue.select())
serialized_obj = []
for obj in all_objects:
serialized_obj.append({

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import logging
import re
import warnings
from asyncio import get_event_loop
from asyncio import get_event_loop, ensure_future, CancelledError
from collections import deque, defaultdict
from functools import partial
from inspect import isawaitable, stack, getmodulename
@ -25,7 +25,8 @@ from sanic.websocket import WebSocketProtocol, ConnectionClosed
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
# end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET:
@ -44,7 +45,7 @@ class Sanic:
self.name = name
self.router = router or Router()
self.error_handler = error_handler or ErrorHandler()
self.config = Config()
self.config = Config(load_env=load_env)
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
@ -54,6 +55,7 @@ class Sanic:
self.listeners = defaultdict(list)
self.is_running = False
self.websocket_enabled = False
self.websocket_tasks = []
# Register alternative method names
self.go_fast = self.run
@ -101,7 +103,8 @@ class Sanic:
return 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
:param uri: path of the URL
@ -117,34 +120,42 @@ class Sanic:
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
host=host, strict_slashes=strict_slashes)
return handler
return response
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=frozenset({"GET"}), host=host)
def get(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"GET"}), host=host,
strict_slashes=strict_slashes)
def post(self, uri, host=None):
return self.route(uri, methods=frozenset({"POST"}), host=host)
def post(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"POST"}), host=host,
strict_slashes=strict_slashes)
def put(self, uri, host=None):
return self.route(uri, methods=frozenset({"PUT"}), host=host)
def put(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"PUT"}), host=host,
strict_slashes=strict_slashes)
def head(self, uri, host=None):
return self.route(uri, methods=frozenset({"HEAD"}), host=host)
def head(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
strict_slashes=strict_slashes)
def options(self, uri, host=None):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
def options(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
strict_slashes=strict_slashes)
def patch(self, uri, host=None):
return self.route(uri, methods=frozenset({"PATCH"}), host=host)
def patch(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
strict_slashes=strict_slashes)
def delete(self, uri, host=None):
return self.route(uri, methods=frozenset({"DELETE"}), host=host)
def delete(self, uri, host=None, strict_slashes=False):
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
functions as a handler to the application url
routes.
@ -168,17 +179,18 @@ class Sanic:
if isinstance(handler, CompositionView):
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
# 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
:param uri: path of the URL
:param host:
:return: decorated function
"""
self.websocket_enabled = True
self.enable_websocket()
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
@ -190,22 +202,31 @@ class Sanic:
request.app = self
protocol = request.transport.get_protocol()
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:
# invoke the application handler
await handler(request, ws, *args, **kwargs)
except ConnectionClosed:
await fut
except (CancelledError, ConnectionClosed):
pass
self.websocket_tasks.remove(fut)
await ws.close()
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 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."""
return self.websocket(uri, host=host)(handler)
return self.websocket(uri, host=host,
strict_slashes=strict_slashes)(handler)
def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket.
@ -213,6 +234,14 @@ class Sanic:
Websocket is enabled automatically if websocket routes are
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
def remove_route(self, uri, clean_cache=True, host=None):
@ -305,7 +334,7 @@ class Sanic:
the output URL's query string.
: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.
:return: the built URL
@ -526,19 +555,24 @@ class Sanic:
if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled
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(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
loop=loop, protocol=protocol, backlog=backlog,
stop_event=stop_event, register_sys_signals=register_sys_signals)
register_sys_signals=register_sys_signals)
try:
self.is_running = True
if workers == 1:
serve(**server_settings)
else:
serve_multiple(server_settings, workers, stop_event)
serve_multiple(server_settings, workers)
except:
log.exception(
'Experienced exception while trying to serve')
@ -550,6 +584,10 @@ class Sanic:
"""This kills the Sanic"""
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,
before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None,
@ -563,13 +601,17 @@ class Sanic:
if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled
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(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock,
loop=loop or get_event_loop(), protocol=protocol,
backlog=backlog, stop_event=stop_event,
run_async=True)
backlog=backlog, run_async=True)
return await serve(**server_settings)
@ -589,7 +631,11 @@ class Sanic:
context = create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(cert, keyfile=key)
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 debug:
warnings.simplefilter('default')
@ -658,6 +704,7 @@ class Sanic:
server_settings['run_async'] = True
# Serve
if host and port:
proto = "http"
if ssl is not None:
proto = "https"

View File

@ -3,7 +3,9 @@ from collections import defaultdict, namedtuple
from sanic.constants import HTTP_METHODS
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'])
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
@ -44,7 +46,8 @@ class Blueprint:
app.route(
uri=uri[1:] if uri.startswith('//') else uri,
methods=future.methods,
host=future.host or self.host
host=future.host or self.host,
strict_slashes=future.strict_slashes
)(future.handler)
for future in self.websocket_routes:
@ -55,7 +58,8 @@ class Blueprint:
uri = url_prefix + future.uri if url_prefix else future.uri
app.websocket(
uri=uri,
host=future.host or self.host
host=future.host or self.host,
strict_slashes=future.strict_slashes
)(future.handler)
# Middleware
@ -82,19 +86,21 @@ class Blueprint:
for listener in listeners:
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.
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
"""
def decorator(handler):
route = FutureRoute(handler, uri, methods, host)
route = FutureRoute(handler, uri, methods, host, strict_slashes)
self.routes.append(route)
return handler
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.
:param handler: function for handling uri requests. Accepts function,
@ -115,16 +121,17 @@ class Blueprint:
if isinstance(handler, CompositionView):
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
def websocket(self, uri, host=None):
def websocket(self, uri, host=None, strict_slashes=False):
"""Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible.
"""
def decorator(handler):
route = FutureRoute(handler, uri, [], host)
route = FutureRoute(handler, uri, [], host, strict_slashes)
self.websocket_routes.append(route)
return handler
return decorator
@ -183,23 +190,30 @@ class Blueprint:
self.statics.append(static)
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=["GET"], host=host)
def get(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["GET"], host=host,
strict_slashes=strict_slashes)
def post(self, uri, host=None):
return self.route(uri, methods=["POST"], host=host)
def post(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["POST"], host=host,
strict_slashes=strict_slashes)
def put(self, uri, host=None):
return self.route(uri, methods=["PUT"], host=host)
def put(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["PUT"], host=host,
strict_slashes=strict_slashes)
def head(self, uri, host=None):
return self.route(uri, methods=["HEAD"], host=host)
def head(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["HEAD"], host=host,
strict_slashes=strict_slashes)
def options(self, uri, host=None):
return self.route(uri, methods=["OPTIONS"], host=host)
def options(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["OPTIONS"], host=host,
strict_slashes=strict_slashes)
def patch(self, uri, host=None):
return self.route(uri, methods=["PATCH"], host=host)
def patch(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["PATCH"], host=host,
strict_slashes=strict_slashes)
def delete(self, uri, host=None):
return self.route(uri, methods=["DELETE"], host=host)
def delete(self, uri, host=None, strict_slashes=False):
return self.route(uri, methods=["DELETE"], host=host,
strict_slashes=strict_slashes)

View File

@ -1,9 +1,12 @@
import os
import types
SANIC_PREFIX = 'SANIC_'
class Config(dict):
def __init__(self, defaults=None):
def __init__(self, defaults=None, load_env=True):
super().__init__(defaults or {})
self.LOGO = """
@ -29,6 +32,9 @@ class Config(dict):
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
self.REQUEST_TIMEOUT = 60 # 60 seconds
if load_env:
self.load_environment_vars()
def __getattr__(self, attr):
try:
return self[attr]
@ -90,3 +96,13 @@ class Config(dict):
for key in dir(obj):
if key.isupper():
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):
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
string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters.

View File

@ -121,6 +121,10 @@ class Request(dict):
self.parsed_args = RequestParameters()
return self.parsed_args
@property
def raw_args(self):
return {k: v[0] for k, v in self.args.items()}
@property
def cookies(self):
if self._cookies is None:
@ -142,10 +146,16 @@ class Request(dict):
@property
def scheme(self):
if self.transport.get_extra_info('sslcontext'):
return 'https'
if self.app.websocket_enabled \
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
def host(self):

View File

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

View File

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

View File

@ -4,10 +4,13 @@ import traceback
import warnings
from functools import partial
from inspect import isawaitable
from multiprocessing import Process, Event
from multiprocessing import Process
from os import set_inheritable
from signal import SIGTERM, SIGINT
from signal import signal as signal_func
from signal import (
SIGTERM, SIGINT,
signal as signal_func,
Signals
)
from socket import socket, SOL_SOCKET, SO_REUSEADDR
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,
request_timeout=60, ssl=None, sock=None, request_max_size=None,
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.
: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)
connections = set()
signal = Signal()
connections = connections if connections is not None else set()
server = partial(
protocol,
loop=loop,
@ -421,7 +424,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
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
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['port'] = None
if stop_event is None:
stop_event = Event()
def sig_handler(signal, frame):
log.info("Received signal {}. Shutting down.".format(
Signals(signal).name))
signal_func(SIGINT, lambda s, f: stop_event.set())
signal_func(SIGTERM, lambda s, f: stop_event.set())
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
processes = []
for _ in range(workers):

View File

@ -22,7 +22,10 @@ class SanicTestClient:
cookies=cookies, connector=conn) as session:
async with getattr(
session, method.lower())(url, *args, **kwargs) as response:
try:
response.text = await response.text()
except UnicodeDecodeError as e:
response.text = None
response.body = await response.read()
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 os
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(
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
@ -35,23 +37,32 @@ setup_kwargs = {
],
}
try:
normal_requirements = [
ujson = 'ujson>=1.35'
uvloop = 'uvloop>=0.5.3'
requirements = [
'httptools>=0.0.9',
'uvloop>=0.5.3',
'ujson>=1.35',
uvloop,
ujson,
'aiofiles>=0.3.0',
'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)
except DistutilsPlatformError as exception:
windows_requirements = [
'httptools>=0.0.9',
'aiofiles>=0.3.0',
'websockets>=3.2',
]
setup_kwargs['install_requires'] = windows_requirements
requirements.remove(ujson)
requirements.remove(uvloop)
print("Installing without uJSON or uvLoop")
setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)
# 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'
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():
app = Sanic('test_text')
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 '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():
app = Sanic('test_load_from_file')

View File

@ -1,49 +1,47 @@
from sanic import Sanic
from sanic.response import text
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.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')
async def handler1(request):
return text('OK')
@data_received_app.exception(PayloadTooLarge)
def handler_exception(request, exception):
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)
assert response.status == 413
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')
async def handler2(request):
return text('OK')
def test_payload_too_large_at_data_received_default():
response = data_received_default_app.test_client.get(
'/1', gather_request=False)
assert response.status == 413
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):
return text('OK')
def test_payload_too_large_at_on_header_default():
data = 'a' * 1000
response = on_header_default_app.test_client.post(
'/1', gather_request=False, data=data)

View File

@ -88,4 +88,7 @@ def test_chained_redirect(redirect_app):
assert request.url.endswith('/1')
assert response.status == 200
assert response.text == 'OK'
try:
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')
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():
app = Sanic('test_route_optional_slash')

View File

@ -25,7 +25,7 @@ def get_file_content(static_file_directory, file_name):
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):
app = Sanic('test_static')
app.static(

View File

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

View File

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