Compare commits

...

69 Commits
0.5.2 ... 0.5.4

Author SHA1 Message Date
Eli Uriegas
bece3d2bcf Merge pull request #702 from seemethere/increment_054
Increment to 0.5.4
2017-05-08 17:44:50 -07:00
Eli Uriegas
307d866bb6 Increment to 0.5.4 2017-05-08 17:44:23 -07:00
Eli Uriegas
95c9514a44 Merge pull request #701 from seemethere/increment_053
Increment to 0.5.3
2017-05-08 17:42:54 -07:00
Eli Uriegas
768be433d6 Increment to 0.5.3 2017-05-08 17:42:26 -07:00
Eli Uriegas
8d3fd75ec2 Merge pull request #695 from kszucs/aiopeewee
aiopeewee example
2017-05-05 11:31:37 -07:00
Szucs Krisztian
a42b254c33 use async version of model_to_dict 2017-05-05 10:51:12 +02:00
Szucs Krisztian
d24e1ae110 removed lines from distributed example 2017-05-05 08:25:18 +02:00
Szucs Krisztian
2e7badab4e aiopeewee example 2017-05-05 08:19:09 +02:00
Raphael Deem
7cf3d49f00 Merge pull request #690 from 38elements/sanic_endpoint_test
Remove utils.py
2017-05-03 23:56:13 -07:00
38elements
25037006bf Remove utils.py 2017-05-04 15:52:18 +09:00
Eli Uriegas
f611eb2c2b Merge pull request #686 from 38elements/loop
Remove loop argument in run() and create_server()
2017-05-03 15:27:16 -07:00
38elements
e12c10b087 Remove loop argument in run() and create_server() 2017-05-03 17:52:19 +09:00
Raphael Deem
9527e5ded8 Merge pull request #685 from r0fls/document-create-server
document create_server method
2017-05-02 22:45:51 -07:00
Raphael Deem
23b4b20b4f document create_server method 2017-05-02 22:44:42 -07:00
Eli Uriegas
7f9ecd659c Merge pull request #682 from graingert/fix-readme.rst-encoding
fix README.rst -> long_description encoding
2017-05-02 10:48:45 -07:00
Eli Uriegas
bb31d465f2 Add distribution types 2017-05-02 10:47:55 -07:00
Thomas Grainger
834468e8e7 fix README.rst -> long_description encoding 2017-05-02 17:37:17 +01:00
Eli Uriegas
4720513672 Merge pull request #679 from graingert/verify-readme-rst-pypi
verify readme for PyPI
2017-05-02 09:31:01 -07:00
Eli Uriegas
a480110d43 Merge pull request #681 from 38elements/deprecation
Remove before_start, before_stop, after_start and after_stop
2017-05-02 09:28:33 -07:00
38elements
0a2c95cc10 Remove before_start, before_stop, after_start and after_stop 2017-05-02 23:07:09 +09:00
Thomas Grainger
9d2e32902d check readme in travis 2017-05-02 10:17:24 +01:00
Thomas Grainger
77b6413526 validate readme for PyPI 2017-05-02 10:05:06 +01:00
Thomas Grainger
9e502099e0 add readme to package directly 2017-05-02 10:05:05 +01:00
Eli Uriegas
c6a7e44ae7 Merge pull request #678 from stopspazzing/patch-1
misspelling
2017-05-01 17:27:31 -07:00
Jeremy Zimmerman
1bf06312b8 misspelling
acutal -> actual
2017-05-01 17:07:38 -07:00
Eli Uriegas
c35721abbd Merge pull request #670 from ticosax/set-exit-code-on-error
In case of error when starting sanic
2017-05-01 15:16:02 -07:00
Eli Uriegas
7f3c417078 Merge pull request #677 from abuckenheimer/master
added exception chain rendering in debug #675
2017-05-01 15:15:34 -07:00
Alec Buckenheimer
69511c2783 added exception chain rendering in debug #675 2017-05-01 12:56:33 -04:00
Eli Uriegas
158a94d34c Merge pull request #674 from 38elements/test-for-uri-template
Add test for uri_template
2017-04-30 20:22:51 -07:00
Eli Uriegas
db58bd68f5 Merge pull request #673 from 38elements/routing
Fix docs/sanic/routing.md
2017-04-30 20:22:30 -07:00
38elements
e65f08a2c8 Fix docs/sanic/routing.md 2017-04-30 22:03:16 +09:00
38elements
ab8f616385 Add test for uri_template 2017-04-30 21:57:32 +09:00
Eli Uriegas
6dc6f9bbb5 Merge pull request #671 from banteg/uri-template
Expose matched request uri template
2017-04-28 14:22:59 -07:00
banteg
7754bb995b expose matched request uri template 2017-04-29 02:39:56 +07:00
Nicolas Delaby
d1fefce61c fixup! In case of error when starting sanic 2017-04-28 20:06:44 +02:00
Nicolas Delaby
c3abdab9c4 The main process that spawn sub processes doesn't run any loop.
let's not try to stop one
2017-04-28 19:57:49 +02:00
Nicolas Delaby
8b13e103fd In case of error when starting sanic
Don't exit with code 0
2017-04-28 19:08:51 +02:00
Eli Uriegas
5f94f65f4f Merge pull request #669 from 38elements/log
Add access.log and error.log to .gitignore
2017-04-28 07:40:42 -07:00
38elements
fc0d69616c Add access.log and error.log to .gitignore 2017-04-28 18:52:15 +09:00
Raphael Deem
656f5b93d6 Merge pull request #668 from seemethere/stop_workers_on_sigint
Add the killing of children
2017-04-27 22:39:48 -07:00
Eli Uriegas
436d37c079 Add the killing of children
Kills children processes when parent process receives a signal to
shutdown.

Solves for #594
2017-04-27 17:47:08 -07:00
Eli Uriegas
ed0081fcf7 Merge pull request #625 from zenixls2/master
based on issue #608, create access log
2017-04-27 14:31:53 -07:00
Eli Uriegas
140062f8a3 Merge pull request #662 from rsrdesarrollo/master
invariant: body after request is processed must be binary
2017-04-27 14:31:28 -07:00
Raphael Deem
75f5fa7c06 Merge pull request #666 from pyx/issue-665
Fix #665 - ImportError not preserved in __main__.py
2017-04-27 00:32:09 -07:00
Philip Xu
9152a1a266 Improved on wording 2017-04-27 03:16:38 -04:00
Philip Xu
ade89ab795 Fix #665 - ImportError not preserved in __main__.py 2017-04-26 22:57:19 -04:00
zenix
95cfdee8b8 Merge branch 'master' of https://github.com/channelcat/sanic 2017-04-26 14:50:42 +09:00
zenix
63a27cc5e2 add document on logging 2017-04-26 14:50:21 +09:00
Eli Uriegas
472face796 Add link to issue tracking sanic projects! 2017-04-25 21:50:49 -07:00
Raphael Deem
8d537a6d0b Merge pull request #663 from mmaybeno/fix_jinja_example_typo
Fix typo for jinja example and converted to dir
2017-04-25 20:29:12 -07:00
Matt Maybeno
b3101d339e Fix typo for jinja example and converted to dir 2017-04-25 20:10:46 -07:00
Raúl Sampedro
85acddddba invariant: body after request is processed must be binary 2017-04-25 12:00:27 +02:00
zenix
c9d747d97f fix merge error 2017-04-25 11:46:13 +09:00
zenix
0bba267808 Merge branch 'master' of https://github.com/channelcat/sanic 2017-04-25 11:07:40 +09:00
Raphael Deem
74cc7be922 Merge branch 'master' into master 2017-04-24 00:47:01 -07:00
zenix
0bbf826b21 fix typo 2017-04-13 16:52:40 +09:00
zenix
02d1900e2f try to fix container error 2017-04-13 16:49:36 +09:00
zenix
73da11b04c switch to use streaming for access log and error log 2017-04-13 16:26:13 +09:00
zenix
4af07e3731 change naming of default log config 2017-04-13 13:49:45 +09:00
zenix
7f60f85cd4 Merge branch 'master' of https://github.com/channelcat/sanic 2017-04-13 13:35:37 +09:00
zenix
c5f137c715 fix original code logic 2017-04-12 18:52:01 +09:00
zenix
66923bc0e3 remove unused param 2017-04-12 18:48:16 +09:00
zenix
8bf7b5a323 remove unused dependency 2017-04-12 18:45:38 +09:00
zenix
36d4d85849 change to use default python config code 2017-04-12 18:44:47 +09:00
zenix
5f0e05f3bf fix flake8 2017-04-12 18:08:06 +09:00
zenix
6fb60ae0b1 Merge branch 'master' of https://github.com/channelcat/sanic 2017-04-12 18:01:12 +09:00
zenix
f872ceb0d9 fix bug in access logging when error happens 2017-04-12 17:39:17 +09:00
zenix
bf46bcf376 Merge branch 'logging' 2017-04-11 19:03:35 +09:00
zenix
f330c3f8c5 add logging based on issue #608, add default config 2017-04-11 18:59:07 +09:00
32 changed files with 578 additions and 150 deletions

2
.gitignore vendored
View File

@@ -14,3 +14,5 @@ settings.py
docs/_build/
docs/_api/
build/*
access.log
error.log

View File

@@ -12,3 +12,4 @@ deploy:
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
on:
tags: true
distributions: "sdist bdist_wheel"

4
MANIFEST.in Normal file
View File

@@ -0,0 +1,4 @@
include README.rst
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

View File

@@ -1,5 +1,5 @@
Sanic
=================================
=====
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
@@ -9,6 +9,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!
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects!
Benchmarks
----------
@@ -84,8 +86,9 @@ Documentation
TODO
----
* Streamed file processing
* http2
* Streamed file processing
* http2
Limitations
-----------
* No wheels for uvloop and httptools on Windows :(

View File

@@ -20,6 +20,7 @@ Guides
sanic/class_based_views
sanic/custom_protocol
sanic/ssl
sanic/logging
sanic/testing
sanic/deploying
sanic/extensions

View File

@@ -1,6 +1,6 @@
# Configuration
Any reasonably complex application will need configuration that is not baked into the acutal code. Settings might be different for different environments or installations.
Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations.
## Basics

View File

@@ -56,3 +56,17 @@ for Gunicorn `worker-class` argument:
```
gunicorn --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
```
## Asynchronous support
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
However be advised that this method does not support using multiple processes, and is not the preferred way
to run the app in general.
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
```python
server = app.create_server(host="0.0.0.0", port=8000)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
loop.run_forever()
```

128
docs/sanic/logging.md Normal file
View File

@@ -0,0 +1,128 @@
# Logging
Sanic allows you to do different types of logging (access log, error log) on the requests based on the [python3 logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on python3 logging if you want do create a new configuration.
### Quck Start
A simple example using default setting would be like this:
```python
from sanic import Sanic
from sanic.config import LOGGING
# The default logging handlers are ['accessStream', 'errorStream']
# but we change it to use other handlers here for demo purpose
LOGGING['loggers']['network']['handlers'] = [
'accessTimedRotatingFile', 'errorTimedRotationgFile']
app = Sanic('test')
@app.route('/')
async def test(request):
return response.text('Hello World!')
if __name__ == "__main__":
app.run(log_config=LOGGING)
```
After the program starts, it will log down all the information/requests in access.log and error.log in your working directory.
And to close logging, simply assign log_config=None:
```python
if __name__ == "__main__":
app.run(log_config=None)
```
This would skip calling logging functions when handling requests.
And you could even do further in production to gain extra speed:
```python
if __name__ == "__main__":
# disable internal messages
app.run(debug=False, log_config=None)
```
### Configuration
By default, log_config parameter is set to use sanic.config.LOGGING dictionary for configuration. The default configuration provides several predefined `handlers`:
- internal (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
For internal information console outputs.
- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
For requests information logging in console
- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
For error message and traceback logging in console.
- accessSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
For requests information logging to syslog.
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
Linux (/dev/log) and FreeBSD (/dev/log).<br>
You would not be able to access this property if the directory doesn't exist.
(Notice that in Docker you have to enable everything by yourself)
- errorSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
For error message and traceback logging to syslog.
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
Linux (/dev/log) and FreeBSD (/dev/log).<br>
You would not be able to access this property if the directory doesn't exist.
(Notice that in Docker you have to enable everything by yourself)
- accessTimedRotatingFile (using [logging.handlers.TimedRotatingFileHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.TimedRotatingFileHandler))<br>
For requests information logging to file with daily rotation support.
- errorTimedRotatingFile (using [logging.handlers.TimedRotatingFileHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.TimedRotatingFileHandler))<br>
For error message and traceback logging to file with daily rotation support.
And `filters`:
- accessFilter (using sanic.defaultFilter.DefaultFilter)<br>
The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)`
- errorFilter (using sanic.defaultFilter.DefaultFilter)<br>
The filter taht allows only levels in `WARNING`, `ERROR`, and `CRITICAL`
There are two `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
- sanic:<br>
Used to log internal messages.
- network:<br>
Used to log requests from network, and any information from those requests.
#### Log format:
In addition to default parameters provided by python (asctime, levelname, message),
Sanic provides additional parameters for network logger with accessFilter:
- host (str)<br>
request.ip
- request (str)<br>
request.method + " " + request.url
- status (int)<br>
response.status
- byte (int)<br>
len(response.body)
The default access log format is
```python
%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d
```

View File

@@ -94,6 +94,7 @@ The following variables are accessible as properties on `Request` objects:
- `host`: The host associated with the request: `localhost:8080`
- `path`: The path of the request: `/posts/1/`
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
- `uri_template`: Template for matching route handler: `/posts/<id>/`
## Accessing values using `get` and `getlist`

View File

@@ -52,7 +52,7 @@ async def integer_handler(request, integer_arg):
async def number_handler(request, number_arg):
return text('Number - {}'.format(number_arg))
@app.route('/person/<name:[A-z]>')
@app.route('/person/<name:[A-z]+>')
async def person_handler(request, name):
return text('Person - {}'.format(name))

View File

@@ -57,17 +57,3 @@ def test_post_json_request_includes_data():
More information about
the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
### Deprecated: `sanic_endpoint_test`
Prior to version 0.3.2, testing was provided through the `sanic_endpoint_test` method. This method will be deprecated in the next major version after 0.4.0; please use the `test_client` instead.
```
from sanic.utils import sanic_endpoint_test
def test_index_returns_200():
request, response = sanic_endpoint_test(app)
assert response.status == 200
```

View File

@@ -1,15 +1,16 @@
# Render templates in a Flask like way from a "template" directory in the project
# Render templates in a Flask like way from a "template" directory in
# the project
from sanic import Sanic
from sanic import response
from jinja2 import Evironment, PackageLoader, select_autoescape
from jinja2 import Environment, PackageLoader, select_autoescape
app = Sanic(__name__)
# Load the template environment with async support
template_env = Environment(
loader=jinja2.PackageLoader('yourapplication', 'templates'),
autoescape=jinja2.select_autoescape(['html', 'xml']),
loader=PackageLoader('jinja_example', 'templates'),
autoescape=select_autoescape(['html', 'xml']),
enable_async=True
)
@@ -19,9 +20,9 @@ template = template_env.get_template("example_template.html")
@app.route('/')
async def test(request):
data = request.json
rendered_template = await template.render_async(**data)
rendered_template = await template.render_async(
knights='that say nih; asynchronously')
return response.html(rendered_template)
app.run(host="0.0.0.0", port=8080, debug=True)
app.run(host="0.0.0.0", port=8080, debug=True)

View File

@@ -0,0 +1,8 @@
aiofiles==0.3.1
httptools==0.0.9
Jinja2==2.9.6
MarkupSafe==1.0
sanic==0.5.2
ujson==1.35
uvloop==0.8.0
websockets==3.3

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Webpage</title>
</head>
<body>
<h1>Hello World</h1>
<p>knights - {{ knights }}</p>
</body>
</html>

View File

@@ -0,0 +1,61 @@
from sanic import Sanic
from sanic.response import json
from aiopeewee import AioModel, AioMySQLDatabase, model_to_dict
from peewee import CharField, TextField, DateTimeField
from peewee import ForeignKeyField, PrimaryKeyField
db = AioMySQLDatabase('test', user='root', password='',
host='127.0.0.1', port=3306)
class User(AioModel):
username = CharField()
class Meta:
database = db
class Blog(AioModel):
user = ForeignKeyField(User)
title = CharField(max_length=25)
content = TextField(default='')
pub_date = DateTimeField(null=True)
pk = PrimaryKeyField()
class Meta:
database = db
app = Sanic(__name__)
@app.listener('before_server_start')
async def setup(app, loop):
# create connection pool
await db.connect(loop)
# create table if not exists
await db.create_tables([User, Blog], safe=True)
@app.listener('before_server_stop')
async def stop(app, loop):
# close connection pool
await db.close()
@app.post('/users')
async def add_user(request):
user = await User.create(**request.json)
return json(await model_to_dict(user))
@app.get('/users/count')
async def user_count(request):
count = await User.select().count()
return json({'count': count})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,6 +1,6 @@
from sanic.app import Sanic
from sanic.blueprints import Blueprint
__version__ = '0.5.2'
__version__ = '0.5.4'
__all__ = ['Sanic', 'Blueprint']

View File

@@ -35,10 +35,10 @@ if __name__ == "__main__":
app.run(host=args.host, port=args.port,
workers=args.workers, debug=args.debug, ssl=ssl)
except ImportError:
except ImportError as e:
log.error("No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
.format(module_name))
.format(e.name))
except ValueError as e:
log.error("{}".format(e))

View File

@@ -1,4 +1,5 @@
import logging
import logging.config
import re
import warnings
from asyncio import get_event_loop, ensure_future, CancelledError
@@ -9,7 +10,7 @@ from traceback import format_exc
from urllib.parse import urlencode, urlunparse
from ssl import create_default_context, Purpose
from sanic.config import Config
from sanic.config import Config, LOGGING
from sanic.constants import HTTP_METHODS
from sanic.exceptions import ServerError, URLBuildError, SanicException
from sanic.handlers import ErrorHandler
@@ -26,7 +27,10 @@ from sanic.websocket import WebSocketProtocol, ConnectionClosed
class Sanic:
def __init__(self, name=None, router=None, error_handler=None,
load_env=True, request_class=None):
load_env=True, request_class=None,
log_config=LOGGING):
if log_config:
logging.config.dictConfig(log_config)
# 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:
@@ -47,6 +51,7 @@ class Sanic:
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
self.log_config = log_config
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
@@ -453,7 +458,8 @@ class Sanic:
# -------------------------------------------- #
# Fetch handler from router
handler, args, kwargs = self.router.get(request)
handler, args, kwargs, uri = self.router.get(request)
request.uri_template = uri
if handler is None:
raise ServerError(
("'None' was returned while requesting a "
@@ -510,36 +516,29 @@ class Sanic:
# Execution
# -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=None,
backlog=100, stop_event=None, register_sys_signals=True):
def run(self, host="127.0.0.1", port=8000, debug=False, ssl=None,
sock=None, workers=1, protocol=None,
backlog=100, stop_event=None, register_sys_signals=True,
log_config=LOGGING):
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param before_start: Functions to be executed before the server starts
accepting connections
:param after_start: Functions to be executed after the server starts
accepting connections
:param before_stop: Functions to be executed when a stop signal is
received before it is respected
:param after_stop: Functions to be executed when all requests are
complete
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param loop:
:param backlog:
:param stop_event:
:param register_sys_signals:
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
if log_config:
logging.config.dictConfig(log_config)
if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol)
@@ -549,11 +548,10 @@ class Sanic:
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,
register_sys_signals=register_sys_signals)
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
workers=workers, protocol=protocol, backlog=backlog,
register_sys_signals=register_sys_signals,
has_log=log_config is not None)
try:
self.is_running = True
@@ -564,6 +562,7 @@ class Sanic:
except:
log.exception(
'Experienced exception while trying to serve')
raise
finally:
self.is_running = False
log.info("Server Stopped")
@@ -577,15 +576,16 @@ class Sanic:
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,
sock=None, loop=None, protocol=None,
backlog=100, stop_event=None):
ssl=None, sock=None, protocol=None,
backlog=100, stop_event=None,
log_config=LOGGING):
"""Asynchronous version of `run`.
NOTE: This does not support multiprocessing and is not the preferred
way to run a Sanic application.
"""
if log_config:
logging.config.dictConfig(log_config)
if protocol is None:
protocol = (WebSocketProtocol if self.websocket_enabled
else HttpProtocol)
@@ -595,11 +595,10 @@ class Sanic:
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, run_async=True)
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
loop=get_event_loop(), protocol=protocol,
backlog=backlog, run_async=True,
has_log=log_config is not None)
return await serve(**server_settings)
@@ -626,10 +625,9 @@ class Sanic:
return response
def _helper(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None, before_stop=None,
after_stop=None, ssl=None, sock=None, workers=1, loop=None,
ssl=None, sock=None, workers=1, loop=None,
protocol=HttpProtocol, backlog=100, stop_event=None,
register_sys_signals=True, run_async=False):
register_sys_signals=True, run_async=False, has_log=True):
"""Helper function used by `run` and `create_server`."""
if isinstance(ssl, dict):
@@ -646,23 +644,6 @@ class Sanic:
warnings.simplefilter('default')
warnings.warn("stop_event will be removed from future versions.",
DeprecationWarning)
if loop is not None:
if debug:
warnings.simplefilter('default')
warnings.warn("Passing a loop will be deprecated in version"
" 0.4.0 https://github.com/channelcat/sanic/"
"pull/335 has more information.",
DeprecationWarning)
# Deprecate this
if any(arg is not None for arg in (after_stop, after_start,
before_start, before_stop)):
if debug:
warnings.simplefilter('default')
warnings.warn("Passing a before_start, before_stop, after_start or"
"after_stop callback will be deprecated in next "
"major version after 0.4.0",
DeprecationWarning)
self.error_handler.debug = debug
self.debug = debug
@@ -683,25 +664,21 @@ class Sanic:
'keep_alive': self.config.KEEP_ALIVE,
'loop': loop,
'register_sys_signals': register_sys_signals,
'backlog': backlog
'backlog': backlog,
'has_log': has_log
}
# -------------------------------------------- #
# Register start/stop events
# -------------------------------------------- #
for event_name, settings_name, reverse, args in (
("before_server_start", "before_start", False, before_start),
("after_server_start", "after_start", False, after_start),
("before_server_stop", "before_stop", True, before_stop),
("after_server_stop", "after_stop", True, after_stop),
for event_name, settings_name, reverse in (
("before_server_start", "before_start", False),
("after_server_start", "after_start", False),
("before_server_stop", "before_stop", True),
("after_server_stop", "after_stop", True),
):
listeners = self.listeners[event_name].copy()
if args:
if callable(args):
listeners.append(args)
else:
listeners.extend(args)
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered

View File

@@ -1,9 +1,119 @@
from sanic.defaultFilter import DefaultFilter
import os
import sys
import syslog
import platform
import types
SANIC_PREFIX = 'SANIC_'
_address_dict = {
'Windows': ('localhost', 514),
'Darwin': '/var/run/syslog',
'Linux': '/dev/log',
'FreeBSD': '/dev/log'
}
LOGGING = {
'version': 1,
'filters': {
'accessFilter': {
'()': DefaultFilter,
'param': [0, 10, 20]
},
'errorFilter': {
'()': DefaultFilter,
'param': [30, 40, 50]
}
},
'formatters': {
'simple': {
'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'
},
'access': {
'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' +
'%(request)s %(message)s %(status)d %(byte)d',
'datefmt': '%Y-%m-%d %H:%M:%S'
}
},
'handlers': {
'internal': {
'class': 'logging.StreamHandler',
'filters': ['accessFilter'],
'formatter': 'simple',
'stream': sys.stderr
},
'accessStream': {
'class': 'logging.StreamHandler',
'filters': ['accessFilter'],
'formatter': 'access',
'stream': sys.stderr
},
'errorStream': {
'class': 'logging.StreamHandler',
'filters': ['errorFilter'],
'formatter': 'simple',
'stream': sys.stderr
},
# before you use accessSysLog, be sure that log levels
# 0, 10, 20 have been enabled in you syslog configuration
# otherwise you won't be able to see the output in syslog
# logging file.
'accessSysLog': {
'class': 'logging.handlers.SysLogHandler',
'address': _address_dict.get(platform.system(),
('localhost', 514)),
'facility': syslog.LOG_DAEMON,
'filters': ['accessFilter'],
'formatter': 'access'
},
'errorSysLog': {
'class': 'logging.handlers.SysLogHandler',
'address': _address_dict.get(platform.system(),
('localhost', 514)),
'facility': syslog.LOG_DAEMON,
'filters': ['errorFilter'],
'formatter': 'simple'
},
'accessTimedRotatingFile': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filters': ['accessFilter'],
'formatter': 'access',
'when': 'D',
'interval': 1,
'backupCount': 7,
'filename': 'access.log'
},
'errorTimedRotatingFile': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filters': ['errorFilter'],
'when': 'D',
'interval': 1,
'backupCount': 7,
'filename': 'error.log',
'formatter': 'simple'
}
},
'loggers': {
'sanic': {
'level': 'DEBUG',
'handlers': ['internal', 'errorStream']
},
'network': {
'level': 'DEBUG',
'handlers': ['accessStream', 'errorStream']
}
}
}
# this happens when using container or systems without syslog
# keep things in config would cause file not exists error
_addr = LOGGING['handlers']['accessSysLog']['address']
if type(_addr) is str and not os.path.exists(_addr):
LOGGING['handlers'].pop('accessSysLog')
LOGGING['handlers'].pop('errorSysLog')
class Config(dict):
def __init__(self, defaults=None, load_env=True, keep_alive=True):

13
sanic/defaultFilter.py Normal file
View File

@@ -0,0 +1,13 @@
import logging
class DefaultFilter(logging.Filter):
def __init__(self, param=None):
self.param = param
def filter(self, record):
if self.param is None:
return True
if record.levelno in self.param:
return True
return False

View File

@@ -47,6 +47,10 @@ TRACEBACK_STYLE = '''
padding: 5px 10px;
}
.tb-border {
padding-top: 20px;
}
.frame-descriptor {
background-color: #e2eafb;
}
@@ -63,12 +67,9 @@ TRACEBACK_WRAPPER_HTML = '''
{style}
</head>
<body>
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
<p class="summary">
{inner_html}
<div class="summary">
<p>
<b>{exc_name}: {exc_value}</b>
while handling path <code>{path}</code>
</p>
@@ -77,6 +78,24 @@ TRACEBACK_WRAPPER_HTML = '''
</html>
'''
TRACEBACK_WRAPPER_INNER_HTML = '''
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
</div>
'''
TRACEBACK_BORDER = '''
<div class="tb-border">
<b><i>
The above exception was the direct cause of the
following exception:
</i></b>
</div>
'''
TRACEBACK_LINE_HTML = '''
<div class="frame-line">
<p class="frame-descriptor">

View File

@@ -9,7 +9,9 @@ from sanic.exceptions import (
SanicException,
TRACEBACK_LINE_HTML,
TRACEBACK_STYLE,
TRACEBACK_WRAPPER_HTML)
TRACEBACK_WRAPPER_HTML,
TRACEBACK_WRAPPER_INNER_HTML,
TRACEBACK_BORDER)
from sanic.log import log
from sanic.response import text, html
@@ -24,19 +26,31 @@ class ErrorHandler:
self.cached_handlers = {}
self.debug = False
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
frames = extract_tb(tb)
def _render_exception(self, exception):
frames = extract_tb(exception.__traceback__)
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
return TRACEBACK_WRAPPER_INNER_HTML.format(
exc_name=exception.__class__.__name__,
exc_value=exception,
frame_html=''.join(frame_html))
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._render_exception(exc_value))
exc_value = exc_value.__cause__
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exc_type.__name__,
exc_value=exc_value,
frame_html=''.join(frame_html),
exc_name=exception.__class__.__name__,
exc_value=exception,
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
path=request.path)
def add(self, exception, handler):

View File

@@ -1,3 +1,4 @@
import logging
log = logging.getLogger('sanic')
netlog = logging.getLogger('network')

View File

@@ -38,7 +38,7 @@ class Request(dict):
__slots__ = (
'app', 'headers', 'version', 'method', '_cookies', 'transport',
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_parsed_url',
'_ip', '_parsed_url', 'uri_template'
)
def __init__(self, url_bytes, headers, version, method, transport):
@@ -57,6 +57,7 @@ class Request(dict):
self.parsed_form = None
self.parsed_files = None
self.parsed_args = None
self.uri_template = None
self._cookies = None
@property

View File

@@ -8,7 +8,7 @@ from sanic.views import CompositionView
Route = namedtuple(
'Route',
['handler', 'methods', 'pattern', 'parameters', 'name'])
['handler', 'methods', 'pattern', 'parameters', 'name', 'uri'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
REGEX_TYPES = {
@@ -225,7 +225,7 @@ class Router:
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters, name=handler_name)
parameters=parameters, name=handler_name, uri=uri)
self.routes_all[uri] = route
if properties['unhashable']:
@@ -344,4 +344,4 @@ class Router:
route_handler = route.handler
if hasattr(route_handler, 'handlers'):
route_handler = route_handler.handlers[method]
return route_handler, [], kwargs
return route_handler, [], kwargs, route.uri

View File

@@ -1,7 +1,6 @@
import asyncio
import os
import traceback
import warnings
from functools import partial
from inspect import isawaitable
from multiprocessing import Process
@@ -25,7 +24,8 @@ try:
except ImportError:
async_loop = asyncio
from sanic.log import log
from sanic.log import log, netlog
from sanic.response import HTTPResponse
from sanic.request import Request
from sanic.exceptions import (
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
@@ -65,12 +65,14 @@ class HttpProtocol(asyncio.Protocol):
# request config
'request_handler', 'request_timeout', 'request_max_size',
'request_class',
# enable or disable access log / error log purpose
'has_log',
# connection management
'_total_request_size', '_timeout_handler', '_last_communication_time')
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60,
request_max_size=None, request_class=None,
request_max_size=None, request_class=None, has_log=True,
keep_alive=True):
self.loop = loop
self.transport = None
@@ -79,6 +81,7 @@ class HttpProtocol(asyncio.Protocol):
self.url = None
self.headers = None
self.signal = signal
self.has_log = has_log
self.connections = connections
self.request_handler = request_handler
self.error_handler = error_handler
@@ -173,8 +176,7 @@ class HttpProtocol(asyncio.Protocol):
self.request.body.append(body)
def on_message_complete(self):
if self.request.body:
self.request.body = b''.join(self.request.body)
self.request.body = b''.join(self.request.body)
self._request_handler_task = self.loop.create_task(
self.request_handler(
@@ -195,6 +197,14 @@ class HttpProtocol(asyncio.Protocol):
response.output(
self.request.version, keep_alive,
self.request_timeout))
if self.has_log:
netlog.info('', extra={
'status': response.status,
'byte': len(response.body),
'host': '%s:%d' % self.request.ip,
'request': '%s %s' % (self.request.method,
self.request.url)
})
except AttributeError:
log.error(
('Invalid response object for url {}, '
@@ -228,6 +238,14 @@ class HttpProtocol(asyncio.Protocol):
response.transport = self.transport
await response.stream(
self.request.version, keep_alive, self.request_timeout)
if self.has_log:
netlog.info('', extra={
'status': response.status,
'byte': -1,
'host': '%s:%d' % self.request.ip,
'request': '%s %s' % (self.request.method,
self.request.url)
})
except AttributeError:
log.error(
('Invalid response object for url {}, '
@@ -263,6 +281,21 @@ class HttpProtocol(asyncio.Protocol):
"Writing error failed, connection closed {}".format(repr(e)),
from_error=True)
finally:
if self.has_log:
extra = {
'status': response.status,
'host': '',
'request': str(self.request) + str(self.url)
}
if response and isinstance(response, HTTPResponse):
extra['byte'] = len(response.body)
else:
extra['byte'] = -1
if self.request:
extra['host'] = '%s:%d' % self.request.ip,
extra['request'] = '%s %s' % (self.request.method,
self.url)
netlog.info('', extra=extra)
self.transport.close()
def bail_out(self, message, from_error=False):
@@ -326,7 +359,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
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, connections=None,
signal=Signal(), request_class=None, keep_alive=True):
signal=Signal(), request_class=None, has_log=True, keep_alive=True):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
@@ -352,6 +385,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
:param loop: asyncio compatible event loop
:param protocol: subclass of asyncio protocol class
:param request_class: Request class to use
:param has_log: disable/enable access log and error log
:return: Nothing
"""
if not run_async:
@@ -374,6 +408,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
request_timeout=request_timeout,
request_max_size=request_max_size,
request_class=request_class,
has_log=has_log,
keep_alive=keep_alive,
)
@@ -445,12 +480,6 @@ def serve_multiple(server_settings, workers):
:param stop_event: if provided, is used as a stop signal
:return:
"""
if server_settings.get('loop', None) is not None:
if server_settings.get('debug', False):
warnings.simplefilter('default')
warnings.warn("Passing a loop will be deprecated in version 0.4.0"
" https://github.com/channelcat/sanic/pull/335"
" has more information.", DeprecationWarning)
server_settings['reuse_port'] = True
# Handling when custom socket is not provided.
@@ -466,6 +495,8 @@ def serve_multiple(server_settings, workers):
def sig_handler(signal, frame):
log.info("Received signal {}. Shutting down.".format(
Signals(signal).name))
for process in processes:
os.kill(process.pid, SIGINT)
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
@@ -484,5 +515,3 @@ def serve_multiple(server_settings, workers):
for process in processes:
process.terminate()
server_settings.get('sock').close()
asyncio.get_event_loop().stop()

View File

@@ -1,17 +0,0 @@
import warnings
from sanic.testing import SanicTestClient
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={},
*request_args, **request_kwargs):
warnings.warn(
"Use of sanic_endpoint_test will be deprecated in"
"the next major version after 0.4.0. Please use the `test_client` "
"available on the app object.", DeprecationWarning)
test_client = SanicTestClient(app)
return test_client._sanic_endpoint_test(
method, uri, gather_request, debug, server_kwargs,
*request_args, **request_kwargs)

View File

@@ -9,14 +9,27 @@ 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:
def open_local(paths, mode='r', encoding='utf8'):
path = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
*paths
)
return codecs.open(path, mode, encoding)
with open_local(['sanic', '__init__.py'], encoding='latin1') as fp:
try:
version = re.findall(r"^__version__ = '([^']+)'\r?$",
fp.read(), re.M)[0]
except IndexError:
raise RuntimeError('Unable to determine version.')
with open_local(['README.rst']) as rm:
long_description = rm.read()
setup_kwargs = {
'name': 'sanic',
'version': version,
@@ -26,6 +39,7 @@ setup_kwargs = {
'author_email': 'channelcat@gmail.com',
'description': (
'A microframework based on uvloop, httptools, and learnings of flask'),
'long_description': long_description,
'packages': ['sanic'],
'platforms': 'any',
'classifiers': [

View File

@@ -1,16 +1,17 @@
import sanic
from sanic.utils import sanic_endpoint_test
from sanic import Sanic
from sanic.response import text
from threading import Event
import asyncio
def test_create_task():
e = Event()
async def coro():
await asyncio.sleep(0.05)
e.set()
app = sanic.Sanic()
app = Sanic('test_create_task')
app.add_task(coro)
@app.route('/early')
@@ -22,9 +23,8 @@ def test_create_task():
await asyncio.sleep(0.1)
return text(e.is_set())
request, response = sanic_endpoint_test(app, uri='/early')
request, response = app.test_client.get('/early')
assert response.body == b'False'
request, response = sanic_endpoint_test(app, uri='/late')
request, response = app.test_client.get('/late')
assert response.body == b'True'

View File

@@ -35,6 +35,15 @@ def handler_5(request):
raise CustomServerError('Custom server error')
@exception_handler_app.route('/6/<arg:int>')
def handler_6(request, arg):
try:
foo = 1 / arg
except Exception as e:
raise e from ValueError("{}".format(arg))
return text(foo)
@exception_handler_app.exception(NotFound, ServerError)
def handler_exception(request, exception):
return text("OK")
@@ -84,6 +93,26 @@ def test_inherited_exception_handler():
assert response.status == 200
def test_chained_exception_handler():
request, response = exception_handler_app.test_client.get(
'/6/0', debug=True)
assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser')
html = str(soup)
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_6' in html
assert 'foo = 1 / arg' in html
assert 'ValueError' in html
assert 'The above exception was the direct cause' in html
summary_text = " ".join(soup.select('.summary')[0].text.split())
assert (
"ZeroDivisionError: division by zero "
"while handling path /6/0") == summary_text
def test_exception_handler_lookup():
class CustomError(Exception):
pass

View File

@@ -133,6 +133,17 @@ def test_query_string():
assert request.args.get('test2') == 'false'
def test_uri_template():
app = Sanic('test_uri_template')
@app.route('/foo/<id:int>/bar/<name:[A-z]+>')
async def handler(request):
return text('OK')
request, response = app.test_client.get('/foo/123/bar/baz')
assert request.uri_template == '/foo/<id:int>/bar/<name:[A-z]+>'
def test_token():
app = Sanic('test_post_token')

13
tox.ini
View File

@@ -1,11 +1,11 @@
[tox]
envlist = py35, py36, flake8
envlist = py35, py36, flake8, check
[travis]
python =
3.5: py35, flake8
3.6: py36, flake8
3.5: py35, flake8, check
3.6: py36, flake8, check
[testenv]
@@ -24,3 +24,10 @@ deps =
commands =
flake8 sanic
[testenv:check]
deps =
docutils
pygments
commands =
python setup.py check -r -s