Compare commits

..

22 Commits
0.1.3 ... 0.1.4

Author SHA1 Message Date
Channel Cat
d961e9d1a2 Merge pull request #58 from channelcat/release-0.1.4
release 0.1.4 - multiprocessing
2016-10-18 02:11:11 -07:00
Channel Cat
8142121c90 Update setup.py 2016-10-18 01:51:17 -07:00
Channel Cat
a904a57fa2 Merge pull request #57 from channelcat/multiprocessing
Added multiprocessing
2016-10-18 01:48:56 -07:00
Channel Cat
4ecb4d2cce Added newline to fix flake8 error 2016-10-18 01:38:50 -07:00
Channel Cat
0a26408c9d Merge pull request #48 from GenericError/patch-1
Improved grammar
2016-10-18 01:32:17 -07:00
Channel Cat
c539933e38 Fixed unused import, added change log 2016-10-18 01:31:09 -07:00
Channel Cat
6f105a647e Added multiprocessing 2016-10-18 01:22:49 -07:00
Channel Cat
18aa937f29 Fix slowdown 2016-10-17 23:34:07 -07:00
Generic Error
625af9a21d Updated capitalisation 2016-10-18 07:04:24 +11:00
Generic Error
0e0d4dd3bc Improved grammar
Improved the grammar and the capitalisation consistency
2016-10-17 20:30:42 +11:00
Channel Cat
0c28cdbaf4 Correcting blueprint documentation
issue #37
2016-10-16 14:28:42 -07:00
Channel Cat
73ef816d89 Merge pull request #45 from mindflayer/master
Fix for string tokens
2016-10-16 14:23:29 -07:00
Channel Cat
031a95e4d9 Merge pull request #39 from seemethere/fix_flake8_errors
Fix flake8 errors
2016-10-16 13:54:36 -07:00
Giorgio Salluzzo
2ee4c0fc6a Merge branch 'master' of github.com:mindflayer/sanic 2016-10-16 22:42:06 +02:00
Giorgio Salluzzo
3e8b8fb46f Fix for issue #44. 2016-10-16 22:41:56 +02:00
Channel Cat
40d8602270 Merge pull request #43 from abhishek7/master
Minor updates to blueprints.md, middleware.md, blueprints.py, and request.py
2016-10-16 13:33:01 -07:00
abhishek7
3c7a8a5f45 Added some documentation to request.py, removed extra line in blueprints.py, and minor grammar enhancements to blueprints.md and middleware.md 2016-10-16 11:35:45 -04:00
Eli Uriegas
bfee7afd0c Remove the 120 line length, reset to default 2016-10-16 08:02:22 -05:00
Eli Uriegas
ea0a037248 Fix flake8 errors 2016-10-16 08:01:59 -05:00
Channel Cat
0148d65dd2 Merge pull request #38 from channelcat/readme-status-images
Adding more sweet readme status images
2016-10-16 04:36:57 -07:00
Channel Cat
8449527ecd Adding more sweet readme status images 2016-10-16 04:36:36 -07:00
Channel Cat
7ceba1ae9d Changed install instructions to use pypi 2016-10-16 02:58:17 -07:00
23 changed files with 436 additions and 109 deletions

View File

@@ -1,13 +1,13 @@
language: python
python:
- '3.5'
- '3.5'
install:
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- python setup.py install
- pip install flake8
- pip install pytest
before_script: flake8 --max-line-length=120 sanic
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- python setup.py install
- pip install flake8
- pip install pytest
before_script: flake8 sanic
script: py.test -v tests
deploy:
provider: pypi

7
CHANGES Normal file
View File

@@ -0,0 +1,7 @@
Version 0.1
-----------
- 0.1.4 - Multiprocessing
- 0.1.3 - Blueprint support
- 0.1.1 - 0.1.2 - Struggling to update pypi via CI
Released to public.

View File

@@ -1,14 +1,18 @@
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
# Sanic
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based off the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/.
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/)
[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/)
On top of being flask-like, sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
## Benchmarks
All tests were run on a AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for falcon and flask, but did not speed up requests.
All tests were run on an AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for Falcon and Flask but did not speed up requests.
| Server | Implementation | Requests/sec | Avg Latency |
| ------- | ------------------- | ------------:| -----------:|
@@ -36,7 +40,7 @@ app.run(host="0.0.0.0", port=8000)
```
## Installation
* `python -m pip install git+https://github.com/channelcat/sanic/`
* `python -m pip install sanic`
## Documentation
* [Getting started](docs/getting_started.md)
@@ -45,6 +49,7 @@ app.run(host="0.0.0.0", port=8000)
* [Middleware](docs/middleware.md)
* [Exceptions](docs/exceptions.md)
* [Blueprints](docs/blueprints.md)
* [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)

View File

@@ -3,7 +3,7 @@
Blueprints are objects that can be used for sub-routing within an application.
Instead of adding routes to the application object, blueprints define similar
methods for adding routes, which are then registered with the application in a
flexible and plugable manner.
flexible and pluggable manner.
## Why?
@@ -56,9 +56,7 @@ In this example, the registered routes in the `app.router` will look like:
```
## Middleware
Using blueprints allows you to also register middleware exclusively for that
blueprint, without interfering with other blueprints or routes registered
directly on the application object.
Using blueprints allows you to also register middleware globally.
```python
@bp.middleware
@@ -75,11 +73,10 @@ async def halt_response(request, response):
```
## Exceptions
Exceptions can also be applied exclusively to blueprints without interfering
with other blueprints or routes registered on the application object.
Exceptions can also be applied exclusively to blueprints globally.
```python
@bp.exception(NotFound)
def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
```
```

35
docs/deploying.md Normal file
View File

@@ -0,0 +1,35 @@
# Deploying
When it comes to deploying Sanic, there's not much to it, but there are
a few things to take note of.
## Workers
By default, Sanic listens in the main process using only 1 CPU core.
To crank up the juice, just specify the number of workers in the run
arguments like so:
```python
app.run(host='0.0.0.0', port=1337, workers=4)
```
Sanic will automatically spin up multiple processes and route
traffic between them. We recommend as many workers as you have
available cores.
## Running via Command
If you like using command line arguments, you can launch a sanic server
by executing the module. For example, if you initialized sanic as
app in a file named server.py, you could run the server like so:
`python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4`
With this way of running sanic, it is not necessary to run app.run in
your python file. If you do, just make sure you wrap it in name == main
like so:
```python
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
```

View File

@@ -4,7 +4,7 @@ Make sure you have pip and python 3.5 before starting
## Benchmarks
* Install Sanic
* `python3 -m pip install git+https://github.com/channelcat/sanic/`
* `python3 -m pip install sanic`
* Edit main.py to include:
```python
from sanic import Sanic
@@ -20,6 +20,6 @@ app.run(host="0.0.0.0", port=8000, debug=True)
```
* Run `python3 main.py`
You now have a working sanic server! To continue on, check out:
You now have a working Sanic server! To continue on, check out:
* [Request Data](request_data.md)
* [Routing](routing.md)

View File

@@ -1,6 +1,6 @@
# Middleware
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware return a response object, the request will stop processing and a response will be returned.
Middleware can be executed before or after requests. It is executed in the order it was registered. If middleware returns a response object, the request will stop processing and a response will be returned.
Middleware is registered via the middleware decorator, and can either be added as 'request' or 'response' middleware, based on the argument provided in the decorator. Response middleware receives both the request and the response as arguments.

View File

@@ -8,3 +8,4 @@ tox
gunicorn
bottle
kyoukai
falcon

36
sanic/__main__.py Normal file
View File

@@ -0,0 +1,36 @@
from argparse import ArgumentParser
from importlib import import_module
from .log import log
from .sanic import Sanic
if __name__ == "__main__":
parser = ArgumentParser(prog='sanic')
parser.add_argument('--host', dest='host', type=str, default='127.0.0.1')
parser.add_argument('--port', dest='port', type=int, default=8000)
parser.add_argument('--workers', dest='workers', type=int, default=1, )
parser.add_argument('--debug', dest='debug', action="store_true")
parser.add_argument('module')
args = parser.parse_args()
try:
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module = import_module(module_name)
app = getattr(module, app_name, None)
if type(app) is not Sanic:
raise ValueError("Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?"
.format(type(app).__name__, args.module))
app.run(host=args.host, port=args.port,
workers=args.workers, debug=args.debug)
except ImportError:
log.error("No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
.format(module_name))
except ValueError as e:
log.error("{}".format(e))

View File

@@ -76,9 +76,9 @@ class Blueprint:
def middleware(self, *args, **kwargs):
"""
"""
def register_middleware(middleware):
self.record(lambda s: s.add_middleware(middleware, *args, **kwargs))
self.record(
lambda s: s.add_middleware(middleware, *args, **kwargs))
return middleware
# Detect which way this was called, @middleware or @middleware('AT')

View File

@@ -44,8 +44,13 @@ class Handler:
def default(self, request, exception):
if issubclass(type(exception), SanicException):
return text("Error: {}".format(exception), status=getattr(exception, 'status_code', 500))
return text(
"Error: {}".format(exception),
status=getattr(exception, 'status_code', 500))
elif self.sanic.debug:
return text("Error: {}\nException: {}".format(exception, format_exc()), status=500)
return text(
"Error: {}\nException: {}".format(
exception, format_exc()), status=500)
else:
return text("An error occurred while generating the request", status=500)
return text(
"An error occurred while generating the request", status=500)

View File

@@ -1,4 +1,5 @@
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
logging.basicConfig(
level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
log = logging.getLogger(__name__)

View File

@@ -26,6 +26,9 @@ class RequestParameters(dict):
class Request:
"""
Properties of an HTTP request such as URL, headers, etc.
"""
__slots__ = (
'url', 'headers', 'version', 'method',
'query_string', 'body',
@@ -39,7 +42,9 @@ class Request:
self.headers = headers
self.version = version
self.method = method
self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None
self.query_string = None
if url_parsed.query:
self.query_string = url_parsed.query.decode('utf-8')
# Init but do not inhale
self.body = None
@@ -53,7 +58,7 @@ class Request:
if not self.parsed_json:
try:
self.parsed_json = json_loads(self.body)
except:
except Exception:
pass
return self.parsed_json
@@ -63,14 +68,19 @@ class Request:
if self.parsed_form is None:
self.parsed_form = {}
self.parsed_files = {}
content_type, parameters = parse_header(self.headers.get('Content-Type'))
content_type, parameters = parse_header(
self.headers.get('Content-Type'))
try:
if content_type is None or content_type == 'application/x-www-form-urlencoded':
self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8')))
is_url_encoded = (
content_type == 'application/x-www-form-urlencoded')
if content_type is None or is_url_encoded:
self.parsed_form = RequestParameters(
parse_qs(self.body.decode('utf-8')))
elif content_type == 'multipart/form-data':
# TODO: Stream this instead of reading to/from memory
boundary = parameters['boundary'].encode('utf-8')
self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary)
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception as e:
log.exception(e)
pass
@@ -88,7 +98,8 @@ class Request:
def args(self):
if self.parsed_args is None:
if self.query_string:
self.parsed_args = RequestParameters(parse_qs(self.query_string))
self.parsed_args = RequestParameters(
parse_qs(self.query_string))
else:
self.parsed_args = {}
@@ -125,7 +136,8 @@ def parse_multipart_form(body, boundary):
colon_index = form_line.index(':')
form_header_field = form_line[0:colon_index]
form_header_value, form_parameters = parse_header(form_line[colon_index + 2:])
form_header_value, form_parameters = parse_header(
form_line[colon_index + 2:])
if form_header_field == 'Content-Disposition':
if 'filename' in form_parameters:
@@ -136,7 +148,8 @@ def parse_multipart_form(body, boundary):
post_data = form_part[line_index:-4]
if file_name or file_type:
files[field_name] = File(type=file_type, name=file_name, body=post_data)
files[field_name] = File(
type=file_type, name=file_name, body=post_data)
else:
fields[field_name] = post_data.decode('utf-8')

View File

@@ -19,7 +19,8 @@ STATUS_CODES = {
class HTTPResponse:
__slots__ = ('body', 'status', 'content_type', 'headers')
def __init__(self, body=None, status=200, headers=None, content_type='text/plain', body_bytes=b''):
def __init__(self, body=None, status=200, headers=None,
content_type='text/plain', body_bytes=b''):
self.content_type = content_type
if body is not None:
@@ -43,7 +44,12 @@ class HTTPResponse:
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
for name, value in self.headers.items()
)
return b'HTTP/%b %d %b\r\nContent-Type: %b\r\nContent-Length: %d\r\nConnection: %b\r\n%b%b\r\n%b' % (
return (b'HTTP/%b %d %b\r\n'
b'Content-Type: %b\r\n'
b'Content-Length: %d\r\n'
b'Connection: %b\r\n'
b'%b%b\r\n'
b'%b') % (
version.encode(),
self.status,
STATUS_CODES.get(self.status, b'FAIL'),
@@ -62,8 +68,10 @@ def json(body, status=200, headers=None):
def text(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8")
return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8")
def html(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8")
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")

View File

@@ -14,17 +14,19 @@ class Router:
def my_route(request, my_parameter):
do stuff...
Parameters will be passed as keyword arguments to the request handling function provided
Parameters can also have a type by appending :type to the <parameter>. If no type is provided,
a string is expected. A regular expression can also be passed in as the type
Parameters will be passed as keyword arguments to the request handling
function provided Parameters can also have a type by appending :type to
the <parameter>. If no type is provided, a string is expected. A regular
expression can also be passed in as the type
TODO:
This probably needs optimization for larger sets of routes,
since it checks every route until it finds a match which is bad and I should feel bad
since it checks every route until it finds a match which is bad and
I should feel bad
"""
routes = None
regex_types = {
"string": (None, "\w+"),
"string": (None, "[^/]+"),
"int": (int, "\d+"),
"number": (float, "[0-9\\.]+"),
"alpha": (None, "[A-Za-z]+"),
@@ -37,13 +39,17 @@ class Router:
"""
Adds a handler to the route list
:param uri: Path to match
:param methods: Array of accepted method names. If none are provided, any method is allowed
:param handler: Request handler function. When executed, it should provide a response object.
:param methods: Array of accepted method names.
If none are provided, any method is allowed
:param handler: Request handler function.
When executed, it should provide a response object.
:return: Nothing
"""
# Dict for faster lookups of if method allowed
methods_dict = {method: True for method in methods} if methods else None
methods_dict = None
if methods:
methods_dict = {method: True for method in methods}
parameters = []
@@ -71,12 +77,15 @@ class Router:
pattern_string = re.sub("<(.+?)>", add_parameter, uri)
pattern = re.compile("^{}$".format(pattern_string))
route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters)
route = Route(
handler=handler, methods=methods_dict, pattern=pattern,
parameters=parameters)
self.routes.append(route)
def get(self, request):
"""
Gets a request handler based on the URL of the request, or raises an error
Gets a request handler based on the URL of the request, or raises an
error
:param request: Request object
:return: handler, arguments, keyword arguments
"""
@@ -89,14 +98,18 @@ class Router:
if match:
for index, parameter in enumerate(_route.parameters, start=1):
value = match.group(index)
kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value
if parameter.cast:
kwargs[parameter.name] = parameter.cast(value)
else:
kwargs[parameter.name] = value
route = _route
break
if route:
if route.methods and request.method not in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
status_code=405)
raise InvalidUsage(
"Method {} not allowed for URL {}".format(
request.method, request.url), status_code=405)
return route.handler, args, kwargs
else:
raise NotFound("Requested URL {} not found".format(request.url))
@@ -114,15 +127,20 @@ class SimpleRouter:
def add(self, uri, methods, handler):
# Dict for faster lookups of method allowed
methods_dict = {method: True for method in methods} if methods else None
self.routes[uri] = Route(handler=handler, methods=methods_dict, pattern=uri, parameters=None)
methods_dict = None
if methods:
methods_dict = {method: True for method in methods}
self.routes[uri] = Route(
handler=handler, methods=methods_dict, pattern=uri,
parameters=None)
def get(self, request):
route = self.routes.get(request.url)
if route:
if route.methods and request.method not in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
status_code=405)
raise InvalidUsage(
"Method {} not allowed for URL {}".format(
request.method, request.url), status_code=405)
return route.handler, [], {}
else:
raise NotFound("Requested URL {} not found".format(request.url))

View File

@@ -1,5 +1,8 @@
import asyncio
from asyncio import get_event_loop
from inspect import isawaitable
from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT
from time import sleep
from traceback import format_exc
from .config import Config
@@ -102,10 +105,12 @@ class Sanic:
async def handle_request(self, request, response_callback):
"""
Takes a request from the HTTP Server and returns a response object to be sent back
The HTTP Server only expects a response object, so exception handling must be done here
Takes a request from the HTTP Server and returns a response object to
be sent back The HTTP Server only expects a response object, so
exception handling must be done here
:param request: HTTP Request object
:param response_callback: Response function to be called with the response as the only argument
:param response_callback: Response function to be called with the
response as the only argument
:return: Nothing
"""
try:
@@ -125,7 +130,9 @@ class Sanic:
# Fetch handler from router
handler, args, kwargs = self.router.get(request)
if handler is None:
raise ServerError("'None' was returned while requesting a handler from the router")
raise ServerError(
("'None' was returned while requesting a "
"handler from the router"))
# Run response handler
response = handler(request, *args, **kwargs)
@@ -149,9 +156,12 @@ class Sanic:
response = await response
except Exception as e:
if self.debug:
response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc()))
response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format(
e, format_exc()))
else:
response = HTTPResponse("An error occured while handling an error")
response = HTTPResponse(
"An error occured while handling an error")
response_callback(response)
@@ -159,20 +169,36 @@ class Sanic:
# Execution
# -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, before_stop=None):
def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None,
before_stop=None, sock=None, workers=1):
"""
Runs the HTTP Server and listens until keyboard interrupt or term signal.
On termination, drains connections before closing.
Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param after_start: Function to be executed after the server starts listening
:param before_stop: Function to be executed when a stop signal is received before it is respected
:param after_start: Function to be executed after the server starts
listening
:param before_stop: Function to be executed when a stop signal is
received before it is respected
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:return: Nothing
"""
self.error_handler.debug = True
self.debug = debug
server_settings = {
'host': host,
'port': port,
'sock': sock,
'debug': debug,
'request_handler': self.handle_request,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
}
if debug:
log.setLevel(logging.DEBUG)
log.debug(self.config.LOGO)
@@ -181,21 +207,61 @@ class Sanic:
log.info('Goin\' Fast @ http://{}:{}'.format(host, port))
try:
serve(
host=host,
port=port,
debug=debug,
after_start=after_start,
before_stop=before_stop,
request_handler=self.handle_request,
request_timeout=self.config.REQUEST_TIMEOUT,
request_max_size=self.config.REQUEST_MAX_SIZE,
)
except:
if workers == 1:
server_settings['after_start'] = after_start
server_settings['before_stop'] = before_stop
serve(**server_settings)
else:
log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers)
except Exception as e:
log.exception(
'Experienced exception while trying to serve: {}'.format(e))
pass
log.info("Server Stopped")
def stop(self):
"""
This kills the Sanic
"""
asyncio.get_event_loop().stop()
get_event_loop().stop()
@staticmethod
def serve_multiple(server_settings, workers, stop_event=None):
"""
Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete.
:param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal
:return:
"""
server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal
if not stop_event:
stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set())
processes = []
for w in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.start()
processes.append(process)
# Infinitely wait for the stop event
try:
while not stop_event.is_set():
sleep(0.3)
except:
pass
log.info('Spinning down workers...')
for process in processes:
process.terminate()
for process in processes:
process.join()

View File

@@ -6,7 +6,7 @@ import httptools
try:
import uvloop as async_loop
except:
except ImportError:
async_loop = asyncio
from .log import log
@@ -18,12 +18,18 @@ class Signal:
class HttpProtocol(asyncio.Protocol):
__slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection
'parser', 'request', 'url', 'headers', # request params
'request_handler', 'request_timeout', 'request_max_size', # request config
'_total_request_size', '_timeout_handler') # connection management
__slots__ = (
# event loop, connection
'loop', 'transport', 'connections', 'signal',
# request params
'parser', 'request', 'url', 'headers',
# request config
'request_handler', 'request_timeout', 'request_max_size',
# connection management
'_total_request_size', '_timeout_handler')
def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60,
def __init__(self, *, loop, request_handler, signal=Signal(),
connections={}, request_timeout=60,
request_max_size=None):
self.loop = loop
self.transport = None
@@ -46,7 +52,8 @@ class HttpProtocol(asyncio.Protocol):
def connection_made(self, transport):
self.connections[self] = True
self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout)
self._timeout_handler = self.loop.call_later(
self.request_timeout, self.connection_timeout)
self.transport = transport
def connection_lost(self, exc):
@@ -63,10 +70,13 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- #
def data_received(self, data):
# Check for the request itself getting too large and exceeding memory limits
# Check for the request itself getting too large and exceeding
# memory limits
self._total_request_size += len(data)
if self._total_request_size > self.request_max_size:
return self.bail_out("Request too large ({}), connection closed".format(self._total_request_size))
return self.bail_out(
"Request too large ({}), connection closed".format(
self._total_request_size))
# Create parser if this is the first time we're receiving data
if self.parser is None:
@@ -78,14 +88,16 @@ class HttpProtocol(asyncio.Protocol):
try:
self.parser.feed_data(data)
except httptools.parser.errors.HttpParserError as e:
self.bail_out("Invalid request data, connection closed ({})".format(e))
self.bail_out(
"Invalid request data, connection closed ({})".format(e))
def on_url(self, url):
self.url = url
def on_header(self, name, value):
if name == b'Content-Length' and int(value) > self.request_max_size:
return self.bail_out("Request body too large ({}), connection closed".format(value))
return self.bail_out(
"Request body too large ({}), connection closed".format(value))
self.headers.append((name.decode(), value.decode('utf-8')))
@@ -101,7 +113,8 @@ class HttpProtocol(asyncio.Protocol):
self.request.body = body
def on_message_complete(self):
self.loop.create_task(self.request_handler(self.request, self.write_response))
self.loop.create_task(
self.request_handler(self.request, self.write_response))
# -------------------------------------------- #
# Responding
@@ -109,14 +122,18 @@ class HttpProtocol(asyncio.Protocol):
def write_response(self, response):
try:
keep_alive = self.parser.should_keep_alive() and not self.signal.stopped
self.transport.write(response.output(self.request.version, keep_alive, self.request_timeout))
keep_alive = self.parser.should_keep_alive() \
and not self.signal.stopped
self.transport.write(
response.output(
self.request.version, keep_alive, self.request_timeout))
if not keep_alive:
self.transport.close()
else:
self.cleanup()
except Exception as e:
self.bail_out("Writing request failed, connection closed {}".format(e))
self.bail_out(
"Writing request failed, connection closed {}".format(e))
def bail_out(self, message):
log.error(message)
@@ -140,8 +157,9 @@ class HttpProtocol(asyncio.Protocol):
return False
def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60,
request_max_size=None):
def serve(host, port, request_handler, after_start=None, before_stop=None,
debug=False, request_timeout=60, sock=None,
request_max_size=None, reuse_port=False):
# Create Event Loop
loop = async_loop.new_event_loop()
asyncio.set_event_loop(loop)
@@ -158,15 +176,12 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
request_handler=request_handler,
request_timeout=request_timeout,
request_max_size=request_max_size,
), host, port)
), host, port, reuse_port=reuse_port, sock=sock)
try:
http_server = loop.run_until_complete(server_coroutine)
except OSError as e:
except Exception as e:
log.error("Unable to start server: {}".format(e))
return
except:
log.exception("Unable to start server")
return
# Run the on_start function if provided
if after_start:
@@ -202,4 +217,3 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
loop.run_until_complete(asyncio.sleep(0.1))
loop.close()
log.info("Server Stopped")

View File

@@ -5,7 +5,7 @@ from setuptools import setup
setup(
name='Sanic',
version="0.1.3",
version="0.1.4",
url='http://github.com/channelcat/sanic/',
license='MIT',
author='Channel Cat',

52
test.py Normal file
View File

@@ -0,0 +1,52 @@
from multiprocessing import Array, Event, Process
from time import sleep
from ujson import loads as json_loads
from sanic import Sanic
from sanic.response import json
from sanic.utils import local_request, HOST, PORT
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_json():
app = Sanic('test_json')
response = Array('c', 50)
@app.route('/')
async def handler(request):
return json({"test": True})
stop_event = Event()
async def after_start(*args, **kwargs):
http_response = await local_request('get', '/')
response.value = http_response.text.encode()
stop_event.set()
def rescue_crew():
sleep(5)
stop_event.set()
rescue_process = Process(target=rescue_crew)
rescue_process.start()
app.serve_multiple({
'host': HOST,
'port': PORT,
'after_start': after_start,
'request_handler': app.handle_request,
'request_max_size': 100000,
}, workers=2, stop_event=stop_event)
rescue_process.terminate()
try:
results = json_loads(response.value)
except:
raise ValueError("Expected JSON response but got '{}'".format(response))
assert results.get('test') == True
test_json()

View File

@@ -0,0 +1,11 @@
# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker falc:app
import falcon
import ujson as json
class TestResource:
def on_get(self, req, resp):
resp.body = json.dumps({"test": True})
app = falcon.API()
app.add_route('/', TestResource())

View File

@@ -15,5 +15,5 @@ app = Sanic("test")
async def test(request):
return json({"test": True})
app.run(host="0.0.0.0", port=sys.argv[1])
if __name__ == '__main__':
app.run(host="0.0.0.0", port=sys.argv[1])

View File

@@ -0,0 +1,53 @@
from multiprocessing import Array, Event, Process
from time import sleep
from ujson import loads as json_loads
from sanic import Sanic
from sanic.response import json
from sanic.utils import local_request, HOST, PORT
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
# TODO: Figure out why this freezes on pytest but not when
# executed via interpreter
def skip_test_multiprocessing():
app = Sanic('test_json')
response = Array('c', 50)
@app.route('/')
async def handler(request):
return json({"test": True})
stop_event = Event()
async def after_start(*args, **kwargs):
http_response = await local_request('get', '/')
response.value = http_response.text.encode()
stop_event.set()
def rescue_crew():
sleep(5)
stop_event.set()
rescue_process = Process(target=rescue_crew)
rescue_process.start()
app.serve_multiple({
'host': HOST,
'port': PORT,
'after_start': after_start,
'request_handler': app.handle_request,
'request_max_size': 100000,
}, workers=2, stop_event=stop_event)
rescue_process.terminate()
try:
results = json_loads(response.value)
except:
raise ValueError("Expected JSON response but got '{}'".format(response))
assert results.get('test') == True

View File

@@ -39,6 +39,11 @@ def test_dynamic_route_string():
assert response.text == 'OK'
assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
assert response.text == 'OK'
assert results[1] == 'favicon.ico'
def test_dynamic_route_int():
app = Sanic('test_dynamic_route_int')