Merge branch 'master' into 594

This commit is contained in:
Raphael Deem 2017-03-29 11:04:08 -07:00 committed by GitHub
commit 9b3bda8d36
21 changed files with 435 additions and 65 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

@ -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:

View File

@ -18,3 +18,4 @@ A list of Sanic extensions created by the community.
`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. - [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-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.

View File

@ -55,8 +55,8 @@ 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')
``` ```

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

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

@ -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 = {}
@ -554,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')
@ -595,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)
@ -621,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')

View File

@ -1,9 +1,11 @@
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 +31,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 +95,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

@ -132,8 +132,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,
@ -331,7 +331,11 @@ def stream(
:param headers: Custom Headers. :param headers: Custom Headers.
""" """
return StreamingHTTPResponse( return StreamingHTTPResponse(
streaming_fn, headers=headers, content_type=content_type, status=status) 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

@ -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
@ -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,13 @@ 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))
loop.close()
signal_func(SIGINT, lambda s, f: loop.close()) signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: loop.close()) signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
processes = [] processes = []
for _ in range(workers): for _ in range(workers):

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

@ -2,48 +2,46 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import PayloadTooLarge from sanic.exceptions import PayloadTooLarge
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(): def test_payload_too_large_from_error_handler():
data_received_app = Sanic('data_received')
data_received_app.config.REQUEST_MAX_SIZE = 1
@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)
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.'
@data_received_default_app.route('/1')
async def handler2(request):
return text('OK')
def test_payload_too_large_at_data_received_default(): 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')
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')
async def handler3(request):
return text('OK')
def test_payload_too_large_at_on_header_default(): 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')
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'
assert response.url.path.endswith('/3') try:
assert response.url.endswith('/3')
except AttributeError:
assert response.url.path.endswith('/3')

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}