4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,11 +1,13 @@ | |||||||
| *~ | *~ | ||||||
| *.egg-info | *.egg-info | ||||||
| *.egg | *.egg | ||||||
|  | *.eggs | ||||||
|  | *.pyc | ||||||
| .coverage | .coverage | ||||||
| .coverage.* | .coverage.* | ||||||
| coverage | coverage | ||||||
| .tox | .tox | ||||||
| settings.py | settings.py | ||||||
| *.pyc |  | ||||||
| .idea/* | .idea/* | ||||||
| .cache/* | .cache/* | ||||||
|  | .python-version | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,14 +1,10 @@ | |||||||
|  | sudo: false | ||||||
| language: python | language: python | ||||||
| python: | python: | ||||||
|   - '3.5' |   - '3.5' | ||||||
| install: |   - '3.6' | ||||||
|   - pip install -r requirements.txt | install: pip install tox-travis | ||||||
|   - pip install -r requirements-dev.txt | script: tox | ||||||
|   - python setup.py install |  | ||||||
|   - pip install flake8 |  | ||||||
|   - pip install pytest |  | ||||||
| before_script: flake8 sanic |  | ||||||
| script: py.test -v tests |  | ||||||
| deploy: | deploy: | ||||||
|   provider: pypi |   provider: pypi | ||||||
|   user: channelcat |   user: channelcat | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,5 +1,7 @@ | |||||||
| # Sanic | # Sanic | ||||||
|  |  | ||||||
|  | [](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||||||
|  |  | ||||||
| [](https://travis-ci.org/channelcat/sanic) | [](https://travis-ci.org/channelcat/sanic) | ||||||
| [](https://pypi.python.org/pypi/sanic/) | [](https://pypi.python.org/pypi/sanic/) | ||||||
| [](https://pypi.python.org/pypi/sanic/) | [](https://pypi.python.org/pypi/sanic/) | ||||||
| @@ -31,13 +33,17 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process.  E | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic.response import json | ||||||
|  |  | ||||||
| app = Sanic(__name__) |  | ||||||
|  | app = Sanic() | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
| async def test(request): | async def test(request): | ||||||
|     return json({ "hello": "world" }) |     return json({"hello": "world"}) | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     app.run(host="0.0.0.0", port=8000) | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000) |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
| @@ -50,8 +56,11 @@ app.run(host="0.0.0.0", port=8000) | |||||||
|  * [Middleware](docs/middleware.md) |  * [Middleware](docs/middleware.md) | ||||||
|  * [Exceptions](docs/exceptions.md) |  * [Exceptions](docs/exceptions.md) | ||||||
|  * [Blueprints](docs/blueprints.md) |  * [Blueprints](docs/blueprints.md) | ||||||
|  |  * [Class Based Views](docs/class_based_views.md) | ||||||
|  * [Cookies](docs/cookies.md) |  * [Cookies](docs/cookies.md) | ||||||
|  * [Static Files](docs/static_files.md) |  * [Static Files](docs/static_files.md) | ||||||
|  |  * [Custom Protocol](docs/custom_protocol.md) | ||||||
|  |  * [Testing](docs/testing.md) | ||||||
|  * [Deploying](docs/deploying.md) |  * [Deploying](docs/deploying.md) | ||||||
|  * [Contributing](docs/contributing.md) |  * [Contributing](docs/contributing.md) | ||||||
|  * [License](LICENSE) |  * [License](LICENSE) | ||||||
| @@ -70,7 +79,7 @@ app.run(host="0.0.0.0", port=8000) | |||||||
|                      ▄▄▄▄▄ |                      ▄▄▄▄▄ | ||||||
|             ▀▀▀██████▄▄▄       _______________ |             ▀▀▀██████▄▄▄       _______________ | ||||||
|           ▄▄▄▄▄  █████████▄  /                 \ |           ▄▄▄▄▄  █████████▄  /                 \ | ||||||
|          ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  |  |          ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  | | ||||||
|        ▀▀█████▄▄ ▀██████▄██ | _________________/ |        ▀▀█████▄▄ ▀██████▄██ | _________________/ | ||||||
|        ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/ |        ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/ | ||||||
|             ▀▀▀▄  ▀▀███ ▀       ▄▄ |             ▀▀▀▄  ▀▀███ ▀       ▄▄ | ||||||
|   | |||||||
							
								
								
									
										58
									
								
								docs/class_based_views.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								docs/class_based_views.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | # Class based views | ||||||
|  |  | ||||||
|  | Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response. | ||||||
|  |  | ||||||
|  | ## Examples | ||||||
|  | ```python | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.views import HTTPMethodView | ||||||
|  | from sanic.response import text | ||||||
|  |  | ||||||
|  | app = Sanic('some_name') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SimpleView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |   def get(self, request): | ||||||
|  |       return text('I am get method') | ||||||
|  |  | ||||||
|  |   def post(self, request): | ||||||
|  |       return text('I am post method') | ||||||
|  |  | ||||||
|  |   def put(self, request): | ||||||
|  |       return text('I am put method') | ||||||
|  |  | ||||||
|  |   def patch(self, request): | ||||||
|  |       return text('I am patch method') | ||||||
|  |  | ||||||
|  |   def delete(self, request): | ||||||
|  |       return text('I am delete method') | ||||||
|  |  | ||||||
|  | app.add_route(SimpleView.as_view(), '/') | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | If you need any url params just mention them in method definition: | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  | class NameView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |   def get(self, request, name): | ||||||
|  |     return text('Hello {}'.format(name)) | ||||||
|  |  | ||||||
|  | app.add_route(NameView.as_view(), '/<name>') | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | If you want to add decorator for class, you could set decorators variable | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | class ViewWithDecorator(HTTPMethodView): | ||||||
|  |   decorators = [some_decorator_here] | ||||||
|  |  | ||||||
|  |   def get(self, request, name): | ||||||
|  |     return text('Hello I have a decorator') | ||||||
|  |  | ||||||
|  | app.add_route(ViewWithDecorator.as_view(), '/url') | ||||||
|  |  | ||||||
|  | ``` | ||||||
							
								
								
									
										70
									
								
								docs/custom_protocol.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								docs/custom_protocol.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | # Custom Protocol | ||||||
|  |  | ||||||
|  | You can change the behavior of protocol by using custom protocol.   | ||||||
|  | If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic. | ||||||
|  |  | ||||||
|  | * loop   | ||||||
|  | `loop` is an asyncio compatible event loop. | ||||||
|  |  | ||||||
|  | * connections   | ||||||
|  | `connections` is a `set object`  to  store protocol objects. | ||||||
|  | When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections. | ||||||
|  |  | ||||||
|  | * signal   | ||||||
|  | `signal` is a `sanic.server.Signal object` with `stopped attribute`. | ||||||
|  | When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. | ||||||
|  |  | ||||||
|  | * request_handler   | ||||||
|  | `request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments. | ||||||
|  |  | ||||||
|  | * error_handler   | ||||||
|  | `error_handler` is a `sanic.exceptions.Handler` object. | ||||||
|  |  | ||||||
|  | * request_timeout   | ||||||
|  | `request_timeout` is seconds for timeout. | ||||||
|  |  | ||||||
|  | * request_max_size   | ||||||
|  | `request_max_size` is bytes of max request size. | ||||||
|  |  | ||||||
|  | ## Example | ||||||
|  | By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.   | ||||||
|  | In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`. | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.server import HttpProtocol | ||||||
|  | from sanic.response import text | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomHttpProtocol(HttpProtocol): | ||||||
|  |  | ||||||
|  |     def __init__(self, *, loop, request_handler, error_handler, | ||||||
|  |                  signal, connections, request_timeout, request_max_size): | ||||||
|  |         super().__init__( | ||||||
|  |             loop=loop, request_handler=request_handler, | ||||||
|  |             error_handler=error_handler, signal=signal, | ||||||
|  |             connections=connections, request_timeout=request_timeout, | ||||||
|  |             request_max_size=request_max_size) | ||||||
|  |  | ||||||
|  |     def write_response(self, response): | ||||||
|  |         if isinstance(response, str): | ||||||
|  |             response = text(response) | ||||||
|  |         self.transport.write( | ||||||
|  |             response.output(self.request.version) | ||||||
|  |         ) | ||||||
|  |         self.transport.close() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route('/') | ||||||
|  | async def string(request): | ||||||
|  |     return 'string' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route('/1') | ||||||
|  | async def response(request): | ||||||
|  |     return text('response') | ||||||
|  |  | ||||||
|  | app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) | ||||||
|  | ``` | ||||||
| @@ -27,3 +27,23 @@ async def handler(request): | |||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000) | app.run(host="0.0.0.0", port=8000) | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ## Middleware chain | ||||||
|  |  | ||||||
|  | If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware: | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  | @app.middleware('response') | ||||||
|  | async def custom_banner(request, response): | ||||||
|  | 	response.headers["Server"] = "Fake-Server" | ||||||
|  |  | ||||||
|  | @app.middleware('response') | ||||||
|  | async def prevent_xss(request, response): | ||||||
|  | 	response.headers["x-xss-protection"] = "1; mode=block" | ||||||
|  |  | ||||||
|  | app.run(host="0.0.0.0", port=8000) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks. | ||||||
|   | |||||||
| @@ -29,4 +29,16 @@ async def person_handler(request, name): | |||||||
| async def folder_handler(request, folder_id): | async def folder_handler(request, folder_id): | ||||||
| 	return text('Folder - {}'.format(folder_id)) | 	return text('Folder - {}'.format(folder_id)) | ||||||
|  |  | ||||||
|  | async def handler1(request): | ||||||
|  | 	return text('OK') | ||||||
|  | app.add_route(handler1, '/test') | ||||||
|  |  | ||||||
|  | async def handler2(request, name): | ||||||
|  | 	return text('Folder - {}'.format(name)) | ||||||
|  | app.add_route(handler2, '/folder/<name>') | ||||||
|  |  | ||||||
|  | async def person_handler2(request, name): | ||||||
|  | 	return text('Person - {}'.format(name)) | ||||||
|  | app.add_route(person_handler2, '/person/<name:[A-z]>') | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								docs/testing.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								docs/testing.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | # Testing | ||||||
|  |  | ||||||
|  | Sanic endpoints can be tested locally using the `sanic.utils` module, which | ||||||
|  | depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/) | ||||||
|  | library. The `sanic_endpoint_test` function runs a local server, issues a | ||||||
|  | configurable request to an endpoint, and returns the result. It takes the | ||||||
|  | following arguments: | ||||||
|  |  | ||||||
|  | - `app` An instance of a Sanic app. | ||||||
|  | - `method` *(default `'get'`)* A string representing the HTTP method to use. | ||||||
|  | - `uri` *(default `'/'`)* A string representing the endpoint to test. | ||||||
|  | - `gather_request` *(default `True`)* A boolean which determines whether the | ||||||
|  |   original request will be returned by the function. If set to `True`, the | ||||||
|  |   return value is a tuple of `(request, response)`, if `False` only the | ||||||
|  |   response is returned. | ||||||
|  | - `loop` *(default `None`)* The event loop to use. | ||||||
|  | - `debug` *(default `False`)* A boolean which determines whether to run the | ||||||
|  |   server in debug mode. | ||||||
|  |  | ||||||
|  | The function further takes the `*request_args` and `**request_kwargs`, which | ||||||
|  | are passed directly to the aiohttp ClientSession request. For example, to | ||||||
|  | supply data with a GET request, `method` would be `get` and the keyword | ||||||
|  | argument `params={'value', 'key'}` would be supplied. 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). | ||||||
|  |  | ||||||
|  | Below is a complete example of an endpoint test, | ||||||
|  | using [pytest](http://doc.pytest.org/en/latest/). The test checks that the | ||||||
|  | `/challenge` endpoint responds to a GET request with a supplied challenge | ||||||
|  | string. | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  | import pytest | ||||||
|  | import aiohttp | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  | # Import the Sanic app, usually created with Sanic(__name__) | ||||||
|  | from external_server import app | ||||||
|  |  | ||||||
|  | def test_endpoint_challenge(): | ||||||
|  |     # Create the challenge data | ||||||
|  |     request_data = {'challenge': 'dummy_challenge'} | ||||||
|  |  | ||||||
|  |     # Send the request to the endpoint, using the default `get` method | ||||||
|  |     request, response = sanic_endpoint_test(app, | ||||||
|  |                                             uri='/challenge', | ||||||
|  |                                             params=request_data) | ||||||
|  |  | ||||||
|  |     # Assert that the server responds with the challenge string | ||||||
|  |     assert response.text == request_data['challenge'] | ||||||
|  | ``` | ||||||
							
								
								
									
										41
									
								
								examples/cache_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								examples/cache_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | """ | ||||||
|  | Example of caching using aiocache package. To run it you will need a Redis | ||||||
|  | instance running in localhost:6379. | ||||||
|  |  | ||||||
|  | Running this example you will see that the first call lasts 3 seconds and | ||||||
|  | the rest are instant because the value is retrieved from the Redis. | ||||||
|  |  | ||||||
|  | If you want more info about the package check | ||||||
|  | https://github.com/argaen/aiocache | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | import aiocache | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import json | ||||||
|  | from sanic.log import log | ||||||
|  | from aiocache import cached | ||||||
|  | from aiocache.serializers import JsonSerializer | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  | aiocache.settings.set_defaults( | ||||||
|  |     cache="aiocache.RedisCache" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @cached(key="my_custom_key", serializer=JsonSerializer()) | ||||||
|  | async def expensive_call(): | ||||||
|  |     log.info("Expensive has been called") | ||||||
|  |     await asyncio.sleep(3) | ||||||
|  |     return {"test": True} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route("/") | ||||||
|  | async def test(request): | ||||||
|  |     log.info("Received GET /") | ||||||
|  |     return json(await expensive_call()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop()) | ||||||
							
								
								
									
										60
									
								
								examples/exception_monitoring.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								examples/exception_monitoring.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | """ | ||||||
|  | Example intercepting uncaught exceptions using Sanic's error handler framework. | ||||||
|  |  | ||||||
|  | This may be useful for developers wishing to use Sentry, Airbrake, etc. | ||||||
|  | or a custom system to log and monitor unexpected errors in production. | ||||||
|  |  | ||||||
|  | First we create our own class inheriting from Handler in sanic.exceptions, | ||||||
|  | and pass in an instance of it when we create our Sanic instance. Inside this | ||||||
|  | class' default handler, we can do anything including sending exceptions to | ||||||
|  | an external service. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Imports and code relevant for our CustomHandler class | ||||||
|  | (Ordinarily this would be in a separate file) | ||||||
|  | """ | ||||||
|  | from sanic.response import text | ||||||
|  | from sanic.exceptions import Handler, SanicException | ||||||
|  |  | ||||||
|  | class CustomHandler(Handler): | ||||||
|  |     def default(self, request, exception): | ||||||
|  |         # Here, we have access to the exception object | ||||||
|  |         # and can do anything with it (log, send to external service, etc) | ||||||
|  |  | ||||||
|  |         # Some exceptions are trivial and built into Sanic (404s, etc) | ||||||
|  |         if not issubclass(type(exception), SanicException): | ||||||
|  |             print(exception) | ||||||
|  |  | ||||||
|  |         # Then, we must finish handling the exception by returning | ||||||
|  |         # our response to the client | ||||||
|  |         # For this we can just call the super class' default handler | ||||||
|  |         return super.default(self, request, exception) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | This is an ordinary Sanic server, with the exception that we set the | ||||||
|  | server's error_handler to an instance of our CustomHandler | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import json | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  | handler = CustomHandler(sanic=app) | ||||||
|  | app.error_handler = handler | ||||||
|  |  | ||||||
|  | @app.route("/") | ||||||
|  | async def test(request): | ||||||
|  |     # Here, something occurs which causes an unexpected exception | ||||||
|  |     # This exception will flow to our custom handler. | ||||||
|  |     x = 1 / 0 | ||||||
|  |     return json({"test": True}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app.run(host="0.0.0.0", port=8000, debug=True) | ||||||
							
								
								
									
										18
									
								
								examples/jinja_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								examples/jinja_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | ## To use this example: | ||||||
|  | # curl -d '{"name": "John Doe"}' localhost:8000 | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import html | ||||||
|  | from jinja2 import Template | ||||||
|  |  | ||||||
|  | template = Template('Hello {{ name }}!') | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  | @app.route('/') | ||||||
|  | async def test(request): | ||||||
|  |     data = request.json | ||||||
|  |     return html(template.render(**data)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | app.run(host="0.0.0.0", port=8000) | ||||||
							
								
								
									
										23
									
								
								examples/override_logging.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								examples/override_logging.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import text | ||||||
|  | import json | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | logging_format = "[%(asctime)s] %(process)d-%(levelname)s " | ||||||
|  | logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " | ||||||
|  | logging_format += "%(message)s" | ||||||
|  |  | ||||||
|  | logging.basicConfig( | ||||||
|  |     format=logging_format, | ||||||
|  |     level=logging.DEBUG | ||||||
|  | ) | ||||||
|  | log = logging.getLogger() | ||||||
|  |  | ||||||
|  | # Set logger to override default basicConfig | ||||||
|  | sanic = Sanic(logger=True) | ||||||
|  | @sanic.route("/") | ||||||
|  | def test(request): | ||||||
|  |     log.info("received request; responding with 'hey'") | ||||||
|  |     return text("hey") | ||||||
|  |  | ||||||
|  | sanic.run(host="0.0.0.0", port=8000) | ||||||
							
								
								
									
										21
									
								
								examples/request_timeout.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								examples/request_timeout.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | import asyncio | ||||||
|  | from sanic.response import text | ||||||
|  | from sanic.config import Config | ||||||
|  | from sanic.exceptions import RequestTimeout | ||||||
|  |  | ||||||
|  | Config.REQUEST_TIMEOUT = 1 | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route('/') | ||||||
|  | async def test(request): | ||||||
|  |     await asyncio.sleep(3) | ||||||
|  |     return text('Hello, world!') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.exception(RequestTimeout) | ||||||
|  | def timeout(request, exception): | ||||||
|  |     return text('RequestTimeout from error_handler.', 408) | ||||||
|  |  | ||||||
|  | app.run(host='0.0.0.0', port=8000) | ||||||
							
								
								
									
										65
									
								
								examples/sanic_aiopg_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								examples/sanic_aiopg_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | """ To run this example you need additional aiopg package | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | import os | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | import uvloop | ||||||
|  | import aiopg | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import json | ||||||
|  |  | ||||||
|  | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||||||
|  |  | ||||||
|  | database_name = os.environ['DATABASE_NAME'] | ||||||
|  | database_host = os.environ['DATABASE_HOST'] | ||||||
|  | database_user = os.environ['DATABASE_USER'] | ||||||
|  | database_password = os.environ['DATABASE_PASSWORD'] | ||||||
|  |  | ||||||
|  | connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, | ||||||
|  |                                                  database_password, | ||||||
|  |                                                  database_host, | ||||||
|  |                                                  database_name) | ||||||
|  | loop = asyncio.get_event_loop() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_pool(): | ||||||
|  |     return await aiopg.create_pool(connection) | ||||||
|  |  | ||||||
|  | app = Sanic(name=__name__) | ||||||
|  | pool = loop.run_until_complete(get_pool()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def prepare_db(): | ||||||
|  |     """ Let's create some table and add some data | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     async with pool.acquire() as conn: | ||||||
|  |         async with conn.cursor() as cur: | ||||||
|  |             await cur.execute('DROP TABLE IF EXISTS sanic_polls') | ||||||
|  |             await cur.execute("""CREATE TABLE sanic_polls ( | ||||||
|  |                                     id serial primary key, | ||||||
|  |                                     question varchar(50), | ||||||
|  |                                     pub_date timestamp | ||||||
|  |                                 );""") | ||||||
|  |             for i in range(0, 100): | ||||||
|  |                 await cur.execute("""INSERT INTO sanic_polls | ||||||
|  |                                 (id, question, pub_date) VALUES ({}, {}, now()) | ||||||
|  |                 """.format(i, i)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route("/") | ||||||
|  | async def handle(request): | ||||||
|  |     async with pool.acquire() as conn: | ||||||
|  |         async with conn.cursor() as cur: | ||||||
|  |             result = [] | ||||||
|  |             await cur.execute("SELECT question, pub_date FROM sanic_polls") | ||||||
|  |             async for row in cur: | ||||||
|  |                 result.append({"question": row[0], "pub_date": row[1]}) | ||||||
|  |             return json({"polls": result}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     loop.run_until_complete(prepare_db()) | ||||||
|  |     app.run(host='0.0.0.0', port=8000, loop=loop) | ||||||
							
								
								
									
										73
									
								
								examples/sanic_aiopg_sqlalchemy_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								examples/sanic_aiopg_sqlalchemy_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | """ To run this example you need additional aiopg package | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | import os | ||||||
|  | import asyncio | ||||||
|  | import datetime | ||||||
|  |  | ||||||
|  | import uvloop | ||||||
|  | from aiopg.sa import create_engine | ||||||
|  | import sqlalchemy as sa | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import json | ||||||
|  |  | ||||||
|  | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||||||
|  |  | ||||||
|  | database_name = os.environ['DATABASE_NAME'] | ||||||
|  | database_host = os.environ['DATABASE_HOST'] | ||||||
|  | database_user = os.environ['DATABASE_USER'] | ||||||
|  | database_password = os.environ['DATABASE_PASSWORD'] | ||||||
|  |  | ||||||
|  | connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, | ||||||
|  |                                                  database_password, | ||||||
|  |                                                  database_host, | ||||||
|  |                                                  database_name) | ||||||
|  | loop = asyncio.get_event_loop() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | metadata = sa.MetaData() | ||||||
|  |  | ||||||
|  | polls = sa.Table('sanic_polls', metadata, | ||||||
|  |                  sa.Column('id', sa.Integer, primary_key=True), | ||||||
|  |                  sa.Column('question', sa.String(50)), | ||||||
|  |                  sa.Column("pub_date", sa.DateTime)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_engine(): | ||||||
|  |     return await create_engine(connection) | ||||||
|  |  | ||||||
|  | app = Sanic(name=__name__) | ||||||
|  | engine = loop.run_until_complete(get_engine()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def prepare_db(): | ||||||
|  |     """ Let's add some data | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     async with engine.acquire() as conn: | ||||||
|  |         await conn.execute('DROP TABLE IF EXISTS sanic_polls') | ||||||
|  |         await conn.execute("""CREATE TABLE sanic_polls ( | ||||||
|  |                                     id serial primary key, | ||||||
|  |                                     question varchar(50), | ||||||
|  |                                     pub_date timestamp | ||||||
|  |                                 );""") | ||||||
|  |         for i in range(0, 100): | ||||||
|  |             await conn.execute( | ||||||
|  |                 polls.insert().values(question=i, | ||||||
|  |                                       pub_date=datetime.datetime.now()) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route("/") | ||||||
|  | async def handle(request): | ||||||
|  |     async with engine.acquire() as conn: | ||||||
|  |         result = [] | ||||||
|  |         async for row in conn.execute(polls.select()): | ||||||
|  |             result.append({"question": row.question, "pub_date": row.pub_date}) | ||||||
|  |         return json({"polls": result}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     loop.run_until_complete(prepare_db()) | ||||||
|  |     app.run(host='0.0.0.0', port=8000, loop=loop) | ||||||
							
								
								
									
										65
									
								
								examples/sanic_asyncpg_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								examples/sanic_asyncpg_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | """ To run this example you need additional asyncpg package | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | import os | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | import uvloop | ||||||
|  | from asyncpg import create_pool | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import json | ||||||
|  |  | ||||||
|  | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||||||
|  |  | ||||||
|  | DB_CONFIG = { | ||||||
|  |     'host': '<host>', | ||||||
|  |     'user': '<username>', | ||||||
|  |     'password': '<password>', | ||||||
|  |     'port': '<port>', | ||||||
|  |     'database': '<database>' | ||||||
|  | } | ||||||
|  |  | ||||||
|  | def jsonify(records): | ||||||
|  |     """ Parse asyncpg record response into JSON format | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     return [{key: value for key, value in | ||||||
|  |              zip(r.keys(), r.values())} for r in records] | ||||||
|  |  | ||||||
|  | loop = asyncio.get_event_loop() | ||||||
|  |  | ||||||
|  | async def make_pool(): | ||||||
|  |     return await create_pool(**DB_CONFIG) | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  | pool = loop.run_until_complete(make_pool()) | ||||||
|  |  | ||||||
|  | async def create_db(): | ||||||
|  |     """ Create some table and add some data | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     async with pool.acquire() as connection: | ||||||
|  |         async with connection.transaction(): | ||||||
|  |             await connection.execute('DROP TABLE IF EXISTS sanic_post') | ||||||
|  |             await connection.execute("""CREATE TABLE sanic_post ( | ||||||
|  |                                     id serial primary key, | ||||||
|  |                                     content varchar(50), | ||||||
|  |                                     post_date timestamp | ||||||
|  |                                 );""") | ||||||
|  |             for i in range(0, 100): | ||||||
|  |                 await connection.execute(f"""INSERT INTO sanic_post | ||||||
|  |                     (id, content, post_date) VALUES ({i}, {i}, now())""") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route("/") | ||||||
|  | async def handler(request): | ||||||
|  |     async with pool.acquire() as connection: | ||||||
|  |         async with connection.transaction(): | ||||||
|  |             results = await connection.fetch('SELECT * FROM sanic_post') | ||||||
|  |             return json({'posts': jsonify(results)}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     loop.run_until_complete(create_db()) | ||||||
|  |     app.run(host='0.0.0.0', port=8000, loop=loop) | ||||||
| @@ -64,11 +64,11 @@ def query_string(request): | |||||||
| # Run Server | # Run Server | ||||||
| # ----------------------------------------------- # | # ----------------------------------------------- # | ||||||
|  |  | ||||||
| def after_start(loop): | def after_start(app, loop): | ||||||
|     log.info("OH OH OH OH OHHHHHHHH") |     log.info("OH OH OH OH OHHHHHHHH") | ||||||
|  |  | ||||||
|  |  | ||||||
| def before_stop(loop): | def before_stop(app, loop): | ||||||
|     log.info("TRIED EVERYTHING") |     log.info("TRIED EVERYTHING") | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ httptools | |||||||
| ujson | ujson | ||||||
| uvloop | uvloop | ||||||
| aiohttp | aiohttp | ||||||
|  | aiocache | ||||||
| pytest | pytest | ||||||
| coverage | coverage | ||||||
| tox | tox | ||||||
| @@ -9,4 +10,5 @@ gunicorn | |||||||
| bottle | bottle | ||||||
| kyoukai | kyoukai | ||||||
| falcon | falcon | ||||||
| tornado | tornado | ||||||
|  | aiofiles | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
| httptools | httptools | ||||||
| ujson | ujson | ||||||
| uvloop | uvloop | ||||||
|  | aiofiles | ||||||
|  | multidict | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from .sanic import Sanic | from .sanic import Sanic | ||||||
| from .blueprints import Blueprint | from .blueprints import Blueprint | ||||||
|  |  | ||||||
| __version__ = '0.1.7' | __version__ = '0.1.9' | ||||||
|  |  | ||||||
| __all__ = ['Sanic', 'Blueprint'] | __all__ = ['Sanic', 'Blueprint'] | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ if __name__ == "__main__": | |||||||
|  |  | ||||||
|         module = import_module(module_name) |         module = import_module(module_name) | ||||||
|         app = getattr(module, app_name, None) |         app = getattr(module, app_name, None) | ||||||
|         if type(app) is not Sanic: |         if not isinstance(app, Sanic): | ||||||
|             raise ValueError("Module is not a Sanic app, it is a {}.  " |             raise ValueError("Module is not a Sanic app, it is a {}.  " | ||||||
|                              "Perhaps you meant {}.app?" |                              "Perhaps you meant {}.app?" | ||||||
|                              .format(type(app).__name__, args.module)) |                              .format(type(app).__name__, args.module)) | ||||||
|   | |||||||
| @@ -91,6 +91,12 @@ class Blueprint: | |||||||
|             return handler |             return handler | ||||||
|         return decorator |         return decorator | ||||||
|  |  | ||||||
|  |     def add_route(self, handler, uri, methods=None): | ||||||
|  |         """ | ||||||
|  |         """ | ||||||
|  |         self.record(lambda s: s.add_route(handler, uri, methods)) | ||||||
|  |         return handler | ||||||
|  |  | ||||||
|     def listener(self, event): |     def listener(self, event): | ||||||
|         """ |         """ | ||||||
|         """ |         """ | ||||||
| @@ -109,8 +115,9 @@ class Blueprint: | |||||||
|  |  | ||||||
|         # Detect which way this was called, @middleware or @middleware('AT') |         # Detect which way this was called, @middleware or @middleware('AT') | ||||||
|         if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): |         if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): | ||||||
|  |             middleware = args[0] | ||||||
|             args = [] |             args = [] | ||||||
|             return register_middleware(args[0]) |             return register_middleware(middleware) | ||||||
|         else: |         else: | ||||||
|             return register_middleware |             return register_middleware | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ def _quote(str): | |||||||
|     else: |     else: | ||||||
|         return '"' + str.translate(_Translator) + '"' |         return '"' + str.translate(_Translator) + '"' | ||||||
|  |  | ||||||
|  |  | ||||||
| _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch | _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| from .response import text | from .response import text | ||||||
|  | from .log import log | ||||||
| from traceback import format_exc | from traceback import format_exc | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -30,6 +31,14 @@ class FileNotFound(NotFound): | |||||||
|         self.relative_url = relative_url |         self.relative_url = relative_url | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestTimeout(SanicException): | ||||||
|  |     status_code = 408 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PayloadTooLarge(SanicException): | ||||||
|  |     status_code = 413 | ||||||
|  |  | ||||||
|  |  | ||||||
| class Handler: | class Handler: | ||||||
|     handlers = None |     handlers = None | ||||||
|  |  | ||||||
| @@ -48,18 +57,31 @@ class Handler: | |||||||
|         :return: Response object |         :return: Response object | ||||||
|         """ |         """ | ||||||
|         handler = self.handlers.get(type(exception), self.default) |         handler = self.handlers.get(type(exception), self.default) | ||||||
|         response = handler(request=request, exception=exception) |         try: | ||||||
|  |             response = handler(request=request, exception=exception) | ||||||
|  |         except: | ||||||
|  |             if self.sanic.debug: | ||||||
|  |                 response_message = ( | ||||||
|  |                     'Exception raised in exception handler "{}" ' | ||||||
|  |                     'for uri: "{}"\n{}').format( | ||||||
|  |                         handler.__name__, request.url, format_exc()) | ||||||
|  |                 log.error(response_message) | ||||||
|  |                 return text(response_message, 500) | ||||||
|  |             else: | ||||||
|  |                 return text('An error occurred while handling an error', 500) | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|     def default(self, request, exception): |     def default(self, request, exception): | ||||||
|         if issubclass(type(exception), SanicException): |         if issubclass(type(exception), SanicException): | ||||||
|             return text( |             return text( | ||||||
|                 "Error: {}".format(exception), |                 'Error: {}'.format(exception), | ||||||
|                 status=getattr(exception, 'status_code', 500)) |                 status=getattr(exception, 'status_code', 500)) | ||||||
|         elif self.sanic.debug: |         elif self.sanic.debug: | ||||||
|             return text( |             response_message = ( | ||||||
|                 "Error: {}\nException: {}".format( |                 'Exception occurred while handling uri: "{}"\n{}'.format( | ||||||
|                     exception, format_exc()), status=500) |                     request.url, format_exc())) | ||||||
|  |             log.error(response_message) | ||||||
|  |             return text(response_message, status=500) | ||||||
|         else: |         else: | ||||||
|             return text( |             return text( | ||||||
|                 "An error occurred while generating the request", status=500) |                 'An error occurred while generating the response', status=500) | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logging.basicConfig( |  | ||||||
|     level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") |  | ||||||
| log = logging.getLogger(__name__) | log = logging.getLogger(__name__) | ||||||
|   | |||||||
| @@ -4,10 +4,17 @@ from http.cookies import SimpleCookie | |||||||
| from httptools import parse_url | from httptools import parse_url | ||||||
| from urllib.parse import parse_qs | from urllib.parse import parse_qs | ||||||
| from ujson import loads as json_loads | from ujson import loads as json_loads | ||||||
|  | from sanic.exceptions import InvalidUsage | ||||||
|  |  | ||||||
| from .log import log | from .log import log | ||||||
|  |  | ||||||
|  |  | ||||||
|  | DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" | ||||||
|  | # HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 | ||||||
|  | # > If the media type remains unknown, the recipient SHOULD treat it | ||||||
|  | # > as type "application/octet-stream" | ||||||
|  |  | ||||||
|  |  | ||||||
| class RequestParameters(dict): | class RequestParameters(dict): | ||||||
|     """ |     """ | ||||||
|     Hosts a dict with lists as values where get returns the first |     Hosts a dict with lists as values where get returns the first | ||||||
| @@ -26,7 +33,7 @@ class RequestParameters(dict): | |||||||
|         return self.super.get(name, default) |         return self.super.get(name, default) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Request: | class Request(dict): | ||||||
|     """ |     """ | ||||||
|     Properties of an HTTP request such as URL, headers, etc. |     Properties of an HTTP request such as URL, headers, etc. | ||||||
|     """ |     """ | ||||||
| @@ -57,25 +64,35 @@ class Request: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def json(self): |     def json(self): | ||||||
|         if not self.parsed_json: |         if self.parsed_json is None: | ||||||
|             try: |             try: | ||||||
|                 self.parsed_json = json_loads(self.body) |                 self.parsed_json = json_loads(self.body) | ||||||
|             except Exception: |             except Exception: | ||||||
|                 pass |                 raise InvalidUsage("Failed when parsing body as json") | ||||||
|  |  | ||||||
|         return self.parsed_json |         return self.parsed_json | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def token(self): | ||||||
|  |         """ | ||||||
|  |         Attempts to return the auth header token. | ||||||
|  |         :return: token related to request | ||||||
|  |         """ | ||||||
|  |         auth_header = self.headers.get('Authorization') | ||||||
|  |         if auth_header is not None: | ||||||
|  |             return auth_header.split()[1] | ||||||
|  |         return auth_header | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def form(self): |     def form(self): | ||||||
|         if self.parsed_form is None: |         if self.parsed_form is None: | ||||||
|             self.parsed_form = {} |             self.parsed_form = RequestParameters() | ||||||
|             self.parsed_files = {} |             self.parsed_files = RequestParameters() | ||||||
|             content_type, parameters = parse_header( |             content_type = self.headers.get( | ||||||
|                 self.headers.get('Content-Type')) |                 'Content-Type', DEFAULT_HTTP_CONTENT_TYPE) | ||||||
|  |             content_type, parameters = parse_header(content_type) | ||||||
|             try: |             try: | ||||||
|                 is_url_encoded = ( |                 if content_type == 'application/x-www-form-urlencoded': | ||||||
|                     content_type == 'application/x-www-form-urlencoded') |  | ||||||
|                 if content_type is None or is_url_encoded: |  | ||||||
|                     self.parsed_form = RequestParameters( |                     self.parsed_form = RequestParameters( | ||||||
|                         parse_qs(self.body.decode('utf-8'))) |                         parse_qs(self.body.decode('utf-8'))) | ||||||
|                 elif content_type == 'multipart/form-data': |                 elif content_type == 'multipart/form-data': | ||||||
| @@ -83,9 +100,8 @@ class Request: | |||||||
|                     boundary = parameters['boundary'].encode('utf-8') |                     boundary = parameters['boundary'].encode('utf-8') | ||||||
|                     self.parsed_form, self.parsed_files = ( |                     self.parsed_form, self.parsed_files = ( | ||||||
|                         parse_multipart_form(self.body, boundary)) |                         parse_multipart_form(self.body, boundary)) | ||||||
|             except Exception as e: |             except Exception: | ||||||
|                 log.exception(e) |                 log.exception("Failed when parsing form") | ||||||
|                 pass |  | ||||||
|  |  | ||||||
|         return self.parsed_form |         return self.parsed_form | ||||||
|  |  | ||||||
| @@ -110,9 +126,10 @@ class Request: | |||||||
|     @property |     @property | ||||||
|     def cookies(self): |     def cookies(self): | ||||||
|         if self._cookies is None: |         if self._cookies is None: | ||||||
|             if 'Cookie' in self.headers: |             cookie = self.headers.get('Cookie') or self.headers.get('cookie') | ||||||
|  |             if cookie is not None: | ||||||
|                 cookies = SimpleCookie() |                 cookies = SimpleCookie() | ||||||
|                 cookies.load(self.headers['Cookie']) |                 cookies.load(cookie) | ||||||
|                 self._cookies = {name: cookie.value |                 self._cookies = {name: cookie.value | ||||||
|                                  for name, cookie in cookies.items()} |                                  for name, cookie in cookies.items()} | ||||||
|             else: |             else: | ||||||
| @@ -128,10 +145,10 @@ def parse_multipart_form(body, boundary): | |||||||
|     Parses a request body and returns fields and files |     Parses a request body and returns fields and files | ||||||
|     :param body: Bytes request body |     :param body: Bytes request body | ||||||
|     :param boundary: Bytes multipart boundary |     :param boundary: Bytes multipart boundary | ||||||
|     :return: fields (dict), files (dict) |     :return: fields (RequestParameters), files (RequestParameters) | ||||||
|     """ |     """ | ||||||
|     files = {} |     files = RequestParameters() | ||||||
|     fields = {} |     fields = RequestParameters() | ||||||
|  |  | ||||||
|     form_parts = body.split(boundary) |     form_parts = body.split(boundary) | ||||||
|     for form_part in form_parts[1:-1]: |     for form_part in form_parts[1:-1]: | ||||||
| @@ -162,9 +179,16 @@ def parse_multipart_form(body, boundary): | |||||||
|  |  | ||||||
|         post_data = form_part[line_index:-4] |         post_data = form_part[line_index:-4] | ||||||
|         if file_name or file_type: |         if file_name or file_type: | ||||||
|             files[field_name] = File( |             file = File(type=file_type, name=file_name, body=post_data) | ||||||
|                 type=file_type, name=file_name, body=post_data) |             if field_name in files: | ||||||
|  |                 files[field_name].append(file) | ||||||
|  |             else: | ||||||
|  |                 files[field_name] = [file] | ||||||
|         else: |         else: | ||||||
|             fields[field_name] = post_data.decode('utf-8') |             value = post_data.decode('utf-8') | ||||||
|  |             if field_name in fields: | ||||||
|  |                 fields[field_name].append(value) | ||||||
|  |             else: | ||||||
|  |                 fields[field_name] = [value] | ||||||
|  |  | ||||||
|     return fields, files |     return fields, files | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
| from aiofiles import open as open_async | from aiofiles import open as open_async | ||||||
| from .cookies import CookieJar |  | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path | ||||||
|  |  | ||||||
| from ujson import dumps as json_dumps | from ujson import dumps as json_dumps | ||||||
|  |  | ||||||
|  | from .cookies import CookieJar | ||||||
|  |  | ||||||
| COMMON_STATUS_CODES = { | COMMON_STATUS_CODES = { | ||||||
|     200: b'OK', |     200: b'OK', | ||||||
|     400: b'Bad Request', |     400: b'Bad Request', | ||||||
| @@ -79,7 +81,12 @@ class HTTPResponse: | |||||||
|         self.content_type = content_type |         self.content_type = content_type | ||||||
|  |  | ||||||
|         if body is not None: |         if body is not None: | ||||||
|             self.body = body.encode('utf-8') |             try: | ||||||
|  |                 # Try to encode it regularly | ||||||
|  |                 self.body = body.encode('utf-8') | ||||||
|  |             except AttributeError: | ||||||
|  |                 # Convert it to a str if you can't | ||||||
|  |                 self.body = str(body).encode('utf-8') | ||||||
|         else: |         else: | ||||||
|             self.body = body_bytes |             self.body = body_bytes | ||||||
|  |  | ||||||
| @@ -96,10 +103,14 @@ class HTTPResponse: | |||||||
|  |  | ||||||
|         headers = b'' |         headers = b'' | ||||||
|         if self.headers: |         if self.headers: | ||||||
|             headers = b''.join( |             for name, value in self.headers.items(): | ||||||
|                 b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) |                 try: | ||||||
|                 for name, value in self.headers.items() |                     headers += ( | ||||||
|             ) |                         b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) | ||||||
|  |                 except AttributeError: | ||||||
|  |                     headers += ( | ||||||
|  |                         b'%b: %b\r\n' % ( | ||||||
|  |                             str(name).encode(), str(value).encode('utf-8'))) | ||||||
|  |  | ||||||
|         # Try to pull from the common codes first |         # Try to pull from the common codes first | ||||||
|         # Speeds up response rate 6% over pulling from all |         # Speeds up response rate 6% over pulling from all | ||||||
|   | |||||||
| @@ -23,18 +23,28 @@ class RouteExists(Exception): | |||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RouteDoesNotExist(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class Router: | class Router: | ||||||
|     """ |     """ | ||||||
|     Router supports basic routing with parameters and method checks |     Router supports basic routing with parameters and method checks | ||||||
|     Usage: |     Usage: | ||||||
|         @sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...]) |         @app.route('/my_url/<my_param>', methods=['GET', 'POST', ...]) | ||||||
|         def my_route(request, my_parameter): |         def my_route(request, my_param): | ||||||
|  |             do stuff... | ||||||
|  |     or | ||||||
|  |         @app.route('/my_url/<my_param:my_type>', methods=['GET', 'POST', ...]) | ||||||
|  |         def my_route_with_type(request, my_param: my_type): | ||||||
|             do stuff... |             do stuff... | ||||||
|  |  | ||||||
|     Parameters will be passed as keyword arguments to the request handling |     Parameters will be passed as keyword arguments to the request handling | ||||||
|     function provided Parameters can also have a type by appending :type to |     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 |     the <parameter>. Given parameter must be able to be type-casted to this. | ||||||
|     expression can also be passed in as the type |     If no type is provided, a string is expected.  A regular expression can | ||||||
|  |     also be passed in as the type. The argument given to the function will | ||||||
|  |     always be a string, independent of the type. | ||||||
|     """ |     """ | ||||||
|     routes_static = None |     routes_static = None | ||||||
|     routes_dynamic = None |     routes_dynamic = None | ||||||
| @@ -103,6 +113,23 @@ class Router: | |||||||
|         else: |         else: | ||||||
|             self.routes_static[uri] = route |             self.routes_static[uri] = route | ||||||
|  |  | ||||||
|  |     def remove(self, uri, clean_cache=True): | ||||||
|  |         try: | ||||||
|  |             route = self.routes_all.pop(uri) | ||||||
|  |         except KeyError: | ||||||
|  |             raise RouteDoesNotExist("Route was not registered: {}".format(uri)) | ||||||
|  |  | ||||||
|  |         if route in self.routes_always_check: | ||||||
|  |             self.routes_always_check.remove(route) | ||||||
|  |         elif url_hash(uri) in self.routes_dynamic \ | ||||||
|  |                 and route in self.routes_dynamic[url_hash(uri)]: | ||||||
|  |             self.routes_dynamic[url_hash(uri)].remove(route) | ||||||
|  |         else: | ||||||
|  |             self.routes_static.pop(uri) | ||||||
|  |  | ||||||
|  |         if clean_cache: | ||||||
|  |             self._get.cache_clear() | ||||||
|  |  | ||||||
|     def get(self, request): |     def get(self, request): | ||||||
|         """ |         """ | ||||||
|         Gets a request handler based on the URL of the request, or raises an |         Gets a request handler based on the URL of the request, or raises an | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								sanic/sanic.py
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								sanic/sanic.py
									
									
									
									
									
								
							| @@ -1,24 +1,35 @@ | |||||||
| from asyncio import get_event_loop | from asyncio import get_event_loop | ||||||
| from collections import deque | from collections import deque | ||||||
| from functools import partial | from functools import partial | ||||||
| from inspect import isawaitable | from inspect import isawaitable, stack, getmodulename | ||||||
| from multiprocessing import Process, Event | from multiprocessing import Process, Event | ||||||
| from signal import signal, SIGTERM, SIGINT | from signal import signal, SIGTERM, SIGINT | ||||||
| from time import sleep |  | ||||||
| from traceback import format_exc | from traceback import format_exc | ||||||
|  | import logging | ||||||
|  |  | ||||||
| from .config import Config | from .config import Config | ||||||
| from .exceptions import Handler | from .exceptions import Handler | ||||||
| from .log import log, logging | from .log import log | ||||||
| from .response import HTTPResponse | from .response import HTTPResponse | ||||||
| from .router import Router | from .router import Router | ||||||
| from .server import serve | from .server import serve, HttpProtocol | ||||||
| from .static import register as static_register | from .static import register as static_register | ||||||
| from .exceptions import ServerError | from .exceptions import ServerError | ||||||
|  | from socket import socket, SOL_SOCKET, SO_REUSEADDR | ||||||
|  | from os import set_inheritable | ||||||
|  |  | ||||||
|  |  | ||||||
| class Sanic: | class Sanic: | ||||||
|     def __init__(self, name, router=None, error_handler=None): |     def __init__(self, name=None, router=None, | ||||||
|  |                  error_handler=None, logger=None): | ||||||
|  |         if logger is None: | ||||||
|  |             logging.basicConfig( | ||||||
|  |                 level=logging.INFO, | ||||||
|  |                 format="%(asctime)s: %(levelname)s: %(message)s" | ||||||
|  |             ) | ||||||
|  |         if name is None: | ||||||
|  |             frame_records = stack()[1] | ||||||
|  |             name = getmodulename(frame_records[1]) | ||||||
|         self.name = name |         self.name = name | ||||||
|         self.router = router or Router() |         self.router = router or Router() | ||||||
|         self.error_handler = error_handler or Handler(self) |         self.error_handler = error_handler or Handler(self) | ||||||
| @@ -29,6 +40,8 @@ class Sanic: | |||||||
|         self._blueprint_order = [] |         self._blueprint_order = [] | ||||||
|         self.loop = None |         self.loop = None | ||||||
|         self.debug = None |         self.debug = None | ||||||
|  |         self.sock = None | ||||||
|  |         self.processes = None | ||||||
|  |  | ||||||
|         # Register alternative method names |         # Register alternative method names | ||||||
|         self.go_fast = self.run |         self.go_fast = self.run | ||||||
| @@ -57,6 +70,22 @@ class Sanic: | |||||||
|  |  | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|  |     def add_route(self, handler, uri, methods=None): | ||||||
|  |         """ | ||||||
|  |         A helper method to register class instance or | ||||||
|  |         functions as a handler to the application url | ||||||
|  |         routes. | ||||||
|  |         :param handler: function or class instance | ||||||
|  |         :param uri: path of the URL | ||||||
|  |         :param methods: list or tuple of methods allowed | ||||||
|  |         :return: function or class instance | ||||||
|  |         """ | ||||||
|  |         self.route(uri=uri, methods=methods)(handler) | ||||||
|  |         return handler | ||||||
|  |  | ||||||
|  |     def remove_route(self, uri, clean_cache=True): | ||||||
|  |         self.router.remove(uri, clean_cache) | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def exception(self, *exceptions): |     def exception(self, *exceptions): | ||||||
|         """ |         """ | ||||||
| @@ -177,18 +206,18 @@ class Sanic: | |||||||
|                 if isawaitable(response): |                 if isawaitable(response): | ||||||
|                     response = await response |                     response = await response | ||||||
|  |  | ||||||
|                 # -------------------------------------------- # |             # -------------------------------------------- # | ||||||
|                 # Response Middleware |             # Response Middleware | ||||||
|                 # -------------------------------------------- # |             # -------------------------------------------- # | ||||||
|  |  | ||||||
|                 if self.response_middleware: |             if self.response_middleware: | ||||||
|                     for middleware in self.response_middleware: |                 for middleware in self.response_middleware: | ||||||
|                         _response = middleware(request, response) |                     _response = middleware(request, response) | ||||||
|                         if isawaitable(_response): |                     if isawaitable(_response): | ||||||
|                             _response = await _response |                         _response = await _response | ||||||
|                         if _response: |                     if _response: | ||||||
|                             response = _response |                         response = _response | ||||||
|                             break |                         break | ||||||
|  |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             # -------------------------------------------- # |             # -------------------------------------------- # | ||||||
| @@ -216,25 +245,27 @@ class Sanic: | |||||||
|  |  | ||||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, |     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, | ||||||
|             after_start=None, before_stop=None, after_stop=None, sock=None, |             after_start=None, before_stop=None, after_stop=None, sock=None, | ||||||
|             workers=1, loop=None): |             workers=1, loop=None, protocol=HttpProtocol, backlog=100, | ||||||
|  |             stop_event=None): | ||||||
|         """ |         """ | ||||||
|         Runs the HTTP Server and listens until keyboard interrupt or term |         Runs the HTTP Server and listens until keyboard interrupt or term | ||||||
|         signal. On termination, drains connections before closing. |         signal. On termination, drains connections before closing. | ||||||
|         :param host: Address to host on |         :param host: Address to host on | ||||||
|         :param port: Port to host on |         :param port: Port to host on | ||||||
|         :param debug: Enables debug output (slows server) |         :param debug: Enables debug output (slows server) | ||||||
|         :param before_start: Function to be executed before the server starts |         :param before_start: Functions to be executed before the server starts | ||||||
|         accepting connections |         accepting connections | ||||||
|         :param after_start: Function to be executed after the server starts |         :param after_start: Functions to be executed after the server starts | ||||||
|         accepting connections |         accepting connections | ||||||
|         :param before_stop: Function to be executed when a stop signal is |         :param before_stop: Functions to be executed when a stop signal is | ||||||
|         received before it is respected |         received before it is respected | ||||||
|         :param after_stop: Function to be executed when all requests are |         :param after_stop: Functions to be executed when all requests are | ||||||
|         complete |         complete | ||||||
|         :param sock: Socket for the server to accept connections from |         :param sock: Socket for the server to accept connections from | ||||||
|         :param workers: Number of processes |         :param workers: Number of processes | ||||||
|         received before it is respected |         received before it is respected | ||||||
|         :param loop: asyncio compatible event loop |         :param loop: asyncio compatible event loop | ||||||
|  |         :param protocol: Subclass of asyncio protocol class | ||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|         self.error_handler.debug = True |         self.error_handler.debug = True | ||||||
| @@ -242,14 +273,17 @@ class Sanic: | |||||||
|         self.loop = loop |         self.loop = loop | ||||||
|  |  | ||||||
|         server_settings = { |         server_settings = { | ||||||
|  |             'protocol': protocol, | ||||||
|             'host': host, |             'host': host, | ||||||
|             'port': port, |             'port': port, | ||||||
|             'sock': sock, |             'sock': sock, | ||||||
|             'debug': debug, |             'debug': debug, | ||||||
|             'request_handler': self.handle_request, |             'request_handler': self.handle_request, | ||||||
|  |             'error_handler': self.error_handler, | ||||||
|             'request_timeout': self.config.REQUEST_TIMEOUT, |             'request_timeout': self.config.REQUEST_TIMEOUT, | ||||||
|             'request_max_size': self.config.REQUEST_MAX_SIZE, |             'request_max_size': self.config.REQUEST_MAX_SIZE, | ||||||
|             'loop': loop |             'loop': loop, | ||||||
|  |             'backlog': backlog | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         # -------------------------------------------- # |         # -------------------------------------------- # | ||||||
| @@ -266,7 +300,7 @@ class Sanic: | |||||||
|             for blueprint in self.blueprints.values(): |             for blueprint in self.blueprints.values(): | ||||||
|                 listeners += blueprint.listeners[event_name] |                 listeners += blueprint.listeners[event_name] | ||||||
|             if args: |             if args: | ||||||
|                 if type(args) is not list: |                 if callable(args): | ||||||
|                     args = [args] |                     args = [args] | ||||||
|                 listeners += args |                 listeners += args | ||||||
|             if reverse: |             if reverse: | ||||||
| @@ -288,12 +322,11 @@ class Sanic: | |||||||
|             else: |             else: | ||||||
|                 log.info('Spinning up {} workers...'.format(workers)) |                 log.info('Spinning up {} workers...'.format(workers)) | ||||||
|  |  | ||||||
|                 self.serve_multiple(server_settings, workers) |                 self.serve_multiple(server_settings, workers, stop_event) | ||||||
|  |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             log.exception( |             log.exception( | ||||||
|                 'Experienced exception while trying to serve: {}'.format(e)) |                 'Experienced exception while trying to serve') | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         log.info("Server Stopped") |         log.info("Server Stopped") | ||||||
|  |  | ||||||
| @@ -301,10 +334,13 @@ class Sanic: | |||||||
|         """ |         """ | ||||||
|         This kills the Sanic |         This kills the Sanic | ||||||
|         """ |         """ | ||||||
|  |         if self.processes is not None: | ||||||
|  |             for process in self.processes: | ||||||
|  |                 process.terminate() | ||||||
|  |             self.sock.close() | ||||||
|         get_event_loop().stop() |         get_event_loop().stop() | ||||||
|  |  | ||||||
|     @staticmethod |     def serve_multiple(self, server_settings, workers, stop_event=None): | ||||||
|     def serve_multiple(server_settings, workers, stop_event=None): |  | ||||||
|         """ |         """ | ||||||
|         Starts multiple server processes simultaneously.  Stops on interrupt |         Starts multiple server processes simultaneously.  Stops on interrupt | ||||||
|         and terminate signals, and drains connections when complete. |         and terminate signals, and drains connections when complete. | ||||||
| @@ -316,26 +352,28 @@ class Sanic: | |||||||
|         server_settings['reuse_port'] = True |         server_settings['reuse_port'] = True | ||||||
|  |  | ||||||
|         # Create a stop event to be triggered by a signal |         # Create a stop event to be triggered by a signal | ||||||
|         if not stop_event: |         if stop_event is None: | ||||||
|             stop_event = Event() |             stop_event = Event() | ||||||
|         signal(SIGINT, lambda s, f: stop_event.set()) |         signal(SIGINT, lambda s, f: stop_event.set()) | ||||||
|         signal(SIGTERM, lambda s, f: stop_event.set()) |         signal(SIGTERM, lambda s, f: stop_event.set()) | ||||||
|  |  | ||||||
|         processes = [] |         self.sock = socket() | ||||||
|  |         self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) | ||||||
|  |         self.sock.bind((server_settings['host'], server_settings['port'])) | ||||||
|  |         set_inheritable(self.sock.fileno(), True) | ||||||
|  |         server_settings['sock'] = self.sock | ||||||
|  |         server_settings['host'] = None | ||||||
|  |         server_settings['port'] = None | ||||||
|  |  | ||||||
|  |         self.processes = [] | ||||||
|         for _ in range(workers): |         for _ in range(workers): | ||||||
|             process = Process(target=serve, kwargs=server_settings) |             process = Process(target=serve, kwargs=server_settings) | ||||||
|  |             process.daemon = True | ||||||
|             process.start() |             process.start() | ||||||
|             processes.append(process) |             self.processes.append(process) | ||||||
|  |  | ||||||
|         # Infinitely wait for the stop event |         for process in self.processes: | ||||||
|         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() |             process.join() | ||||||
|  |  | ||||||
|  |         # the above processes will block this until they're stopped | ||||||
|  |         self.stop() | ||||||
|   | |||||||
							
								
								
									
										136
									
								
								sanic/server.py
									
									
									
									
									
								
							
							
						
						
									
										136
									
								
								sanic/server.py
									
									
									
									
									
								
							| @@ -1,8 +1,12 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  | from functools import partial | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
|  | from multidict import CIMultiDict | ||||||
| from signal import SIGINT, SIGTERM | from signal import SIGINT, SIGTERM | ||||||
|  | from time import time | ||||||
| import httptools | from httptools import HttpRequestParser | ||||||
|  | from httptools.parser.errors import HttpParserError | ||||||
|  | from .exceptions import ServerError | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     import uvloop as async_loop |     import uvloop as async_loop | ||||||
| @@ -11,12 +15,16 @@ except ImportError: | |||||||
|  |  | ||||||
| from .log import log | from .log import log | ||||||
| from .request import Request | from .request import Request | ||||||
|  | from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage | ||||||
|  |  | ||||||
|  |  | ||||||
| class Signal: | class Signal: | ||||||
|     stopped = False |     stopped = False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | current_time = None | ||||||
|  |  | ||||||
|  |  | ||||||
| class HttpProtocol(asyncio.Protocol): | class HttpProtocol(asyncio.Protocol): | ||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         # event loop, connection |         # event loop, connection | ||||||
| @@ -26,10 +34,10 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         # request config |         # request config | ||||||
|         'request_handler', 'request_timeout', 'request_max_size', |         'request_handler', 'request_timeout', 'request_max_size', | ||||||
|         # connection management |         # connection management | ||||||
|         '_total_request_size', '_timeout_handler') |         '_total_request_size', '_timeout_handler', '_last_communication_time') | ||||||
|  |  | ||||||
|     def __init__(self, *, loop, request_handler, signal=Signal(), |     def __init__(self, *, loop, request_handler, error_handler, | ||||||
|                  connections={}, request_timeout=60, |                  signal=Signal(), connections={}, request_timeout=60, | ||||||
|                  request_max_size=None): |                  request_max_size=None): | ||||||
|         self.loop = loop |         self.loop = loop | ||||||
|         self.transport = None |         self.transport = None | ||||||
| @@ -40,32 +48,44 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.signal = signal |         self.signal = signal | ||||||
|         self.connections = connections |         self.connections = connections | ||||||
|         self.request_handler = request_handler |         self.request_handler = request_handler | ||||||
|  |         self.error_handler = error_handler | ||||||
|         self.request_timeout = request_timeout |         self.request_timeout = request_timeout | ||||||
|         self.request_max_size = request_max_size |         self.request_max_size = request_max_size | ||||||
|         self._total_request_size = 0 |         self._total_request_size = 0 | ||||||
|         self._timeout_handler = None |         self._timeout_handler = None | ||||||
|  |         self._last_request_time = None | ||||||
|  |         self._request_handler_task = None | ||||||
|  |  | ||||||
|         # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|  |  | ||||||
|     # Connection |     # Connection | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|  |  | ||||||
|     def connection_made(self, transport): |     def connection_made(self, transport): | ||||||
|         self.connections[self] = True |         self.connections.add(self) | ||||||
|         self._timeout_handler = self.loop.call_later( |         self._timeout_handler = self.loop.call_later( | ||||||
|             self.request_timeout, self.connection_timeout) |             self.request_timeout, self.connection_timeout) | ||||||
|         self.transport = transport |         self.transport = transport | ||||||
|  |         self._last_request_time = current_time | ||||||
|  |  | ||||||
|     def connection_lost(self, exc): |     def connection_lost(self, exc): | ||||||
|         del self.connections[self] |         self.connections.discard(self) | ||||||
|         self._timeout_handler.cancel() |         self._timeout_handler.cancel() | ||||||
|         self.cleanup() |         self.cleanup() | ||||||
|  |  | ||||||
|     def connection_timeout(self): |     def connection_timeout(self): | ||||||
|         self.bail_out("Request timed out, connection closed") |         # Check if | ||||||
|  |         time_elapsed = current_time - self._last_request_time | ||||||
|         # -------------------------------------------- # |         if time_elapsed < self.request_timeout: | ||||||
|  |             time_left = self.request_timeout - time_elapsed | ||||||
|  |             self._timeout_handler = \ | ||||||
|  |                 self.loop.call_later(time_left, self.connection_timeout) | ||||||
|  |         else: | ||||||
|  |             if self._request_handler_task: | ||||||
|  |                 self._request_handler_task.cancel() | ||||||
|  |             exception = RequestTimeout('Request Timeout') | ||||||
|  |             self.write_error(exception) | ||||||
|  |  | ||||||
|  |     # -------------------------------------------- # | ||||||
|     # Parsing |     # Parsing | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|  |  | ||||||
| @@ -74,37 +94,40 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         # memory limits |         # memory limits | ||||||
|         self._total_request_size += len(data) |         self._total_request_size += len(data) | ||||||
|         if self._total_request_size > self.request_max_size: |         if self._total_request_size > self.request_max_size: | ||||||
|             return self.bail_out( |             exception = PayloadTooLarge('Payload Too Large') | ||||||
|                 "Request too large ({}), connection closed".format( |             self.write_error(exception) | ||||||
|                     self._total_request_size)) |  | ||||||
|  |  | ||||||
|         # Create parser if this is the first time we're receiving data |         # Create parser if this is the first time we're receiving data | ||||||
|         if self.parser is None: |         if self.parser is None: | ||||||
|             assert self.request is None |             assert self.request is None | ||||||
|             self.headers = [] |             self.headers = [] | ||||||
|             self.parser = httptools.HttpRequestParser(self) |             self.parser = HttpRequestParser(self) | ||||||
|  |  | ||||||
|         # Parse request chunk or close connection |         # Parse request chunk or close connection | ||||||
|         try: |         try: | ||||||
|             self.parser.feed_data(data) |             self.parser.feed_data(data) | ||||||
|         except httptools.parser.errors.HttpParserError as e: |         except HttpParserError: | ||||||
|             self.bail_out( |             exception = InvalidUsage('Bad Request') | ||||||
|                 "Invalid request data, connection closed ({})".format(e)) |             self.write_error(exception) | ||||||
|  |  | ||||||
|     def on_url(self, url): |     def on_url(self, url): | ||||||
|         self.url = url |         self.url = url | ||||||
|  |  | ||||||
|     def on_header(self, name, value): |     def on_header(self, name, value): | ||||||
|         if name == b'Content-Length' and int(value) > self.request_max_size: |         if name == b'Content-Length' and int(value) > self.request_max_size: | ||||||
|             return self.bail_out( |             exception = PayloadTooLarge('Payload Too Large') | ||||||
|                 "Request body too large ({}), connection closed".format(value)) |             self.write_error(exception) | ||||||
|  |  | ||||||
|         self.headers.append((name.decode(), value.decode('utf-8'))) |         self.headers.append((name.decode(), value.decode('utf-8'))) | ||||||
|  |  | ||||||
|     def on_headers_complete(self): |     def on_headers_complete(self): | ||||||
|  |         remote_addr = self.transport.get_extra_info('peername') | ||||||
|  |         if remote_addr: | ||||||
|  |             self.headers.append(('Remote-Addr', '%s:%s' % remote_addr)) | ||||||
|  |  | ||||||
|         self.request = Request( |         self.request = Request( | ||||||
|             url_bytes=self.url, |             url_bytes=self.url, | ||||||
|             headers=dict(self.headers), |             headers=CIMultiDict(self.headers), | ||||||
|             version=self.parser.get_http_version(), |             version=self.parser.get_http_version(), | ||||||
|             method=self.parser.get_method().decode() |             method=self.parser.get_method().decode() | ||||||
|         ) |         ) | ||||||
| @@ -116,7 +139,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             self.request.body = body |             self.request.body = body | ||||||
|  |  | ||||||
|     def on_message_complete(self): |     def on_message_complete(self): | ||||||
|         self.loop.create_task( |         self._request_handler_task = self.loop.create_task( | ||||||
|             self.request_handler(self.request, self.write_response)) |             self.request_handler(self.request, self.write_response)) | ||||||
|  |  | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
| @@ -133,20 +156,34 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             if not keep_alive: |             if not keep_alive: | ||||||
|                 self.transport.close() |                 self.transport.close() | ||||||
|             else: |             else: | ||||||
|  |                 # Record that we received data | ||||||
|  |                 self._last_request_time = current_time | ||||||
|                 self.cleanup() |                 self.cleanup() | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             self.bail_out( |             self.bail_out( | ||||||
|                 "Writing request failed, connection closed {}".format(e)) |                 "Writing response failed, connection closed {}".format(e)) | ||||||
|  |  | ||||||
|  |     def write_error(self, exception): | ||||||
|  |         try: | ||||||
|  |             response = self.error_handler.response(self.request, exception) | ||||||
|  |             version = self.request.version if self.request else '1.1' | ||||||
|  |             self.transport.write(response.output(version)) | ||||||
|  |             self.transport.close() | ||||||
|  |         except Exception as e: | ||||||
|  |             self.bail_out( | ||||||
|  |                 "Writing error failed, connection closed {}".format(e)) | ||||||
|  |  | ||||||
|     def bail_out(self, message): |     def bail_out(self, message): | ||||||
|  |         exception = ServerError(message) | ||||||
|  |         self.write_error(exception) | ||||||
|         log.error(message) |         log.error(message) | ||||||
|         self.transport.close() |  | ||||||
|  |  | ||||||
|     def cleanup(self): |     def cleanup(self): | ||||||
|         self.parser = None |         self.parser = None | ||||||
|         self.request = None |         self.request = None | ||||||
|         self.url = None |         self.url = None | ||||||
|         self.headers = None |         self.headers = None | ||||||
|  |         self._request_handler_task = None | ||||||
|         self._total_request_size = 0 |         self._total_request_size = 0 | ||||||
|  |  | ||||||
|     def close_if_idle(self): |     def close_if_idle(self): | ||||||
| @@ -160,6 +197,18 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         return False |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def update_current_time(loop): | ||||||
|  |     """ | ||||||
|  |     Caches the current time, since it is needed | ||||||
|  |     at the end of every keep-alive request to update the request timeout time | ||||||
|  |     :param loop: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     global current_time | ||||||
|  |     current_time = time() | ||||||
|  |     loop.call_later(1, partial(update_current_time, loop)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def trigger_events(events, loop): | def trigger_events(events, loop): | ||||||
|     """ |     """ | ||||||
|     :param events: one or more sync or async functions to execute |     :param events: one or more sync or async functions to execute | ||||||
| @@ -174,25 +223,31 @@ def trigger_events(events, loop): | |||||||
|                 loop.run_until_complete(result) |                 loop.run_until_complete(result) | ||||||
|  |  | ||||||
|  |  | ||||||
| def serve(host, port, request_handler, before_start=None, after_start=None, | def serve(host, port, request_handler, error_handler, before_start=None, | ||||||
|           before_stop=None, after_stop=None, |           after_start=None, before_stop=None, after_stop=None, debug=False, | ||||||
|           debug=False, request_timeout=60, sock=None, |           request_timeout=60, sock=None, request_max_size=None, | ||||||
|           request_max_size=None, reuse_port=False, loop=None): |           reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100): | ||||||
|     """ |     """ | ||||||
|     Starts asynchronous HTTP Server on an individual process. |     Starts asynchronous HTTP Server on an individual process. | ||||||
|     :param host: Address to host on |     :param host: Address to host on | ||||||
|     :param port: Port to host on |     :param port: Port to host on | ||||||
|     :param request_handler: Sanic request handler with middleware |     :param request_handler: Sanic request handler with middleware | ||||||
|  |     :param error_handler: Sanic error handler with middleware | ||||||
|  |     :param before_start: Function to be executed before the server starts | ||||||
|  |     listening. Takes single argument `loop` | ||||||
|     :param after_start: Function to be executed after the server starts |     :param after_start: Function to be executed after the server starts | ||||||
|     listening. Takes single argument `loop` |     listening. Takes single argument `loop` | ||||||
|     :param before_stop: Function to be executed when a stop signal is |     :param before_stop: Function to be executed when a stop signal is | ||||||
|     received before it is respected. Takes single argumenet `loop` |     received before it is respected. Takes single argumenet `loop` | ||||||
|  |     :param after_stop: Function to be executed when a stop signal is | ||||||
|  |     received after it is respected. Takes single argumenet `loop` | ||||||
|     :param debug: Enables debug output (slows server) |     :param debug: Enables debug output (slows server) | ||||||
|     :param request_timeout: time in seconds |     :param request_timeout: time in seconds | ||||||
|     :param sock: Socket for the server to accept connections from |     :param sock: Socket for the server to accept connections from | ||||||
|     :param request_max_size: size in bytes, `None` for no limit |     :param request_max_size: size in bytes, `None` for no limit | ||||||
|     :param reuse_port: `True` for multiple workers |     :param reuse_port: `True` for multiple workers | ||||||
|     :param loop: asyncio compatible event loop |     :param loop: asyncio compatible event loop | ||||||
|  |     :param protocol: Subclass of asyncio protocol class | ||||||
|     :return: Nothing |     :return: Nothing | ||||||
|     """ |     """ | ||||||
|     loop = loop or async_loop.new_event_loop() |     loop = loop or async_loop.new_event_loop() | ||||||
| @@ -203,16 +258,31 @@ def serve(host, port, request_handler, before_start=None, after_start=None, | |||||||
|  |  | ||||||
|     trigger_events(before_start, loop) |     trigger_events(before_start, loop) | ||||||
|  |  | ||||||
|     connections = {} |     connections = set() | ||||||
|     signal = Signal() |     signal = Signal() | ||||||
|     server_coroutine = loop.create_server(lambda: HttpProtocol( |     server = partial( | ||||||
|  |         protocol, | ||||||
|         loop=loop, |         loop=loop, | ||||||
|         connections=connections, |         connections=connections, | ||||||
|         signal=signal, |         signal=signal, | ||||||
|         request_handler=request_handler, |         request_handler=request_handler, | ||||||
|  |         error_handler=error_handler, | ||||||
|         request_timeout=request_timeout, |         request_timeout=request_timeout, | ||||||
|         request_max_size=request_max_size, |         request_max_size=request_max_size, | ||||||
|     ), host, port, reuse_port=reuse_port, sock=sock) |     ) | ||||||
|  |  | ||||||
|  |     server_coroutine = loop.create_server( | ||||||
|  |         server, | ||||||
|  |         host, | ||||||
|  |         port, | ||||||
|  |         reuse_port=reuse_port, | ||||||
|  |         sock=sock, | ||||||
|  |         backlog=backlog | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Instead of pulling time at the end of every request, | ||||||
|  |     # pull it once per minute | ||||||
|  |     loop.call_soon(partial(update_current_time, loop)) | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         http_server = loop.run_until_complete(server_coroutine) |         http_server = loop.run_until_complete(server_coroutine) | ||||||
| @@ -240,7 +310,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, | |||||||
|  |  | ||||||
|         # Complete all tasks on the loop |         # Complete all tasks on the loop | ||||||
|         signal.stopped = True |         signal.stopped = True | ||||||
|         for connection in connections.keys(): |         for connection in connections: | ||||||
|             connection.close_if_idle() |             connection.close_if_idle() | ||||||
|  |  | ||||||
|         while connections: |         while connections: | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ from aiofiles.os import stat | |||||||
| from os import path | from os import path | ||||||
| from re import sub | from re import sub | ||||||
| from time import strftime, gmtime | from time import strftime, gmtime | ||||||
|  | from urllib.parse import unquote | ||||||
|  |  | ||||||
| from .exceptions import FileNotFound, InvalidUsage | from .exceptions import FileNotFound, InvalidUsage | ||||||
| from .response import file, HTTPResponse | from .response import file, HTTPResponse | ||||||
| @@ -32,12 +33,17 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): | |||||||
|         # served.  os.path.realpath seems to be very slow |         # served.  os.path.realpath seems to be very slow | ||||||
|         if file_uri and '../' in file_uri: |         if file_uri and '../' in file_uri: | ||||||
|             raise InvalidUsage("Invalid URL") |             raise InvalidUsage("Invalid URL") | ||||||
|  |  | ||||||
|         # Merge served directory and requested file if provided |         # Merge served directory and requested file if provided | ||||||
|         # Strip all / that in the beginning of the URL to help prevent python |         # Strip all / that in the beginning of the URL to help prevent python | ||||||
|         # from herping a derp and treating the uri as an absolute path |         # from herping a derp and treating the uri as an absolute path | ||||||
|         file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ |         file_path = file_or_directory | ||||||
|             if file_uri else file_or_directory |         if file_uri: | ||||||
|  |             file_path = path.join( | ||||||
|  |                 file_or_directory, sub('^[/]*', '', file_uri)) | ||||||
|  |  | ||||||
|  |         # URL decode the path sent by the browser otherwise we won't be able to | ||||||
|  |         # match filenames which got encoded (filenames with spaces etc) | ||||||
|  |         file_path = unquote(file_path) | ||||||
|         try: |         try: | ||||||
|             headers = {} |             headers = {} | ||||||
|             # Check if the client has been sent this file before |             # Check if the client has been sent this file before | ||||||
|   | |||||||
| @@ -16,14 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): | |||||||
|  |  | ||||||
|  |  | ||||||
| def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||||
|                         loop=None, *request_args, **request_kwargs): |                         loop=None, debug=False, server_kwargs={}, | ||||||
|  |                         *request_args, **request_kwargs): | ||||||
|     results = [] |     results = [] | ||||||
|     exceptions = [] |     exceptions = [] | ||||||
|  |  | ||||||
|     if gather_request: |     if gather_request: | ||||||
|         @app.middleware |  | ||||||
|         def _collect_request(request): |         def _collect_request(request): | ||||||
|             results.append(request) |             results.append(request) | ||||||
|  |         app.request_middleware.appendleft(_collect_request) | ||||||
|  |  | ||||||
|     async def _collect_response(sanic, loop): |     async def _collect_response(sanic, loop): | ||||||
|         try: |         try: | ||||||
| @@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | |||||||
|             exceptions.append(e) |             exceptions.append(e) | ||||||
|         app.stop() |         app.stop() | ||||||
|  |  | ||||||
|     app.run(host=HOST, port=42101, after_start=_collect_response, loop=loop) |     app.run(host=HOST, debug=debug, port=PORT, | ||||||
|  |             after_start=_collect_response, loop=loop, **server_kwargs) | ||||||
|  |  | ||||||
|     if exceptions: |     if exceptions: | ||||||
|         raise ValueError("Exception during request: {}".format(exceptions)) |         raise ValueError("Exception during request: {}".format(exceptions)) | ||||||
| @@ -45,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | |||||||
|             return request, response |             return request, response | ||||||
|         except: |         except: | ||||||
|             raise ValueError( |             raise ValueError( | ||||||
|                 "request and response object expected, got ({})".format( |                 "Request and response object expected, got ({})".format( | ||||||
|                     results)) |                     results)) | ||||||
|     else: |     else: | ||||||
|         try: |         try: | ||||||
|             return results[0] |             return results[0] | ||||||
|         except: |         except: | ||||||
|             raise ValueError( |             raise ValueError( | ||||||
|                 "request object expected, got ({})".format(results)) |                 "Request object expected, got ({})".format(results)) | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								sanic/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								sanic/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | from .exceptions import InvalidUsage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPMethodView: | ||||||
|  |     """ Simple class based implementation of view for the sanic. | ||||||
|  |     You should implement methods (get, post, put, patch, delete) for the class | ||||||
|  |     to every HTTP method you want to support. | ||||||
|  |  | ||||||
|  |     For example: | ||||||
|  |         class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |             def get(self, request, *args, **kwargs): | ||||||
|  |                 return text('I am get method') | ||||||
|  |  | ||||||
|  |             def put(self, request, *args, **kwargs): | ||||||
|  |                 return text('I am put method') | ||||||
|  |     etc. | ||||||
|  |  | ||||||
|  |     If someone tries to use a non-implemented method, there will be a | ||||||
|  |     405 response. | ||||||
|  |  | ||||||
|  |     If you need any url params just mention them in method definition: | ||||||
|  |         class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |             def get(self, request, my_param_here, *args, **kwargs): | ||||||
|  |                 return text('I am get method with %s' % my_param_here) | ||||||
|  |  | ||||||
|  |     To add the view into the routing you could use | ||||||
|  |         1) app.add_route(DummyView.as_view(), '/') | ||||||
|  |         2) app.route('/')(DummyView.as_view()) | ||||||
|  |  | ||||||
|  |     To add any decorator you could set it into decorators variable | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     decorators = [] | ||||||
|  |  | ||||||
|  |     def dispatch_request(self, request, *args, **kwargs): | ||||||
|  |         handler = getattr(self, request.method.lower(), None) | ||||||
|  |         if handler: | ||||||
|  |             return handler(request, *args, **kwargs) | ||||||
|  |         raise InvalidUsage( | ||||||
|  |             'Method {} not allowed for URL {}'.format( | ||||||
|  |                 request.method, request.url), status_code=405) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def as_view(cls, *class_args, **class_kwargs): | ||||||
|  |         """ Converts the class into an actual view function that can be used | ||||||
|  |         with the routing system. | ||||||
|  |  | ||||||
|  |         """ | ||||||
|  |         def view(*args, **kwargs): | ||||||
|  |             self = view.view_class(*class_args, **class_kwargs) | ||||||
|  |             return self.dispatch_request(*args, **kwargs) | ||||||
|  |  | ||||||
|  |         if cls.decorators: | ||||||
|  |             view.__module__ = cls.__module__ | ||||||
|  |             for decorator in cls.decorators: | ||||||
|  |                 view = decorator(view) | ||||||
|  |  | ||||||
|  |         view.view_class = cls | ||||||
|  |         view.__doc__ = cls.__doc__ | ||||||
|  |         view.__module__ = cls.__module__ | ||||||
|  |         return view | ||||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| @@ -30,6 +30,7 @@ setup( | |||||||
|         'httptools>=0.0.9', |         'httptools>=0.0.9', | ||||||
|         'ujson>=1.35', |         'ujson>=1.35', | ||||||
|         'aiofiles>=0.3.0', |         'aiofiles>=0.3.0', | ||||||
|  |         'multidict>=2.0', | ||||||
|     ], |     ], | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|         'Development Status :: 2 - Pre-Alpha', |         'Development Status :: 2 - Pre-Alpha', | ||||||
|   | |||||||
| @@ -15,4 +15,4 @@ async def handle(request): | |||||||
| app = web.Application(loop=loop) | app = web.Application(loop=loop) | ||||||
| app.router.add_route('GET', '/', handle) | app.router.add_route('GET', '/', handle) | ||||||
|  |  | ||||||
| web.run_app(app, port=sys.argv[1]) | web.run_app(app, port=sys.argv[1], access_log=None) | ||||||
|   | |||||||
| @@ -1,16 +1,30 @@ | |||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|     "fmt" | 	"encoding/json" | ||||||
|     "os" | 	"net/http" | ||||||
|     "net/http" | 	"os" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | type TestJSONResponse struct { | ||||||
|  | 	Test bool | ||||||
|  | } | ||||||
|  |  | ||||||
| func handler(w http.ResponseWriter, r *http.Request) { | func handler(w http.ResponseWriter, r *http.Request) { | ||||||
|     fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) | 	response := TestJSONResponse{true} | ||||||
|  |  | ||||||
|  | 	js, err := json.Marshal(response) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	w.Header().Set("Content-Type", "application/json") | ||||||
|  | 	w.Write(js) | ||||||
| } | } | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
|     http.HandleFunc("/", handler) | 	http.HandleFunc("/", handler) | ||||||
|     http.ListenAndServe(":" + os.Args[1], nil) | 	http.ListenAndServe(":"+os.Args[1], nil) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								tests/static/decode me.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/static/decode me.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | I need to be decoded as a uri | ||||||
							
								
								
									
										1
									
								
								tests/static/test.file
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/static/test.file
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | I am just a regular static file | ||||||
							
								
								
									
										20
									
								
								tests/test_bad_request.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								tests/test_bad_request.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | import asyncio | ||||||
|  | from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_bad_request_response(): | ||||||
|  |     app = Sanic('test_bad_request_response') | ||||||
|  |     lines = [] | ||||||
|  |     async def _request(sanic, loop): | ||||||
|  |         connect = asyncio.open_connection('127.0.0.1', 42101) | ||||||
|  |         reader, writer = await connect | ||||||
|  |         writer.write(b'not http') | ||||||
|  |         while True: | ||||||
|  |             line = await reader.readline() | ||||||
|  |             if not line: | ||||||
|  |                 break | ||||||
|  |             lines.append(line) | ||||||
|  |         app.stop() | ||||||
|  |     app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request) | ||||||
|  |     assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n' | ||||||
|  |     assert lines[-1] == b'Error: Bad Request' | ||||||
| @@ -25,6 +25,19 @@ def test_cookies(): | |||||||
|     assert response.text == 'Cookies are: working!' |     assert response.text == 'Cookies are: working!' | ||||||
|     assert response_cookies['right_back'].value == 'at you' |     assert response_cookies['right_back'].value == 'at you' | ||||||
|  |  | ||||||
|  | def test_http2_cookies(): | ||||||
|  |     app = Sanic('test_http2_cookies') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         response = text('Cookies are: {}'.format(request.cookies['test'])) | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  |     headers = {'cookie': 'test=working!'} | ||||||
|  |     request, response = sanic_endpoint_test(app, headers=headers) | ||||||
|  |  | ||||||
|  |     assert response.text == 'Cookies are: working!' | ||||||
|  |  | ||||||
| def test_cookie_options(): | def test_cookie_options(): | ||||||
|     app = Sanic('test_text') |     app = Sanic('test_text') | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								tests/test_custom_protocol.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								tests/test_custom_protocol.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.server import HttpProtocol | ||||||
|  | from sanic.response import text | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  | app = Sanic('test_custom_porotocol') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomHttpProtocol(HttpProtocol): | ||||||
|  |  | ||||||
|  |     def write_response(self, response): | ||||||
|  |         if isinstance(response, str): | ||||||
|  |             response = text(response) | ||||||
|  |         self.transport.write( | ||||||
|  |             response.output(self.request.version) | ||||||
|  |         ) | ||||||
|  |         self.transport.close() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route('/1') | ||||||
|  | async def handler_1(request): | ||||||
|  |     return 'OK' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_use_custom_protocol(): | ||||||
|  |     server_kwargs = { | ||||||
|  |         'protocol': CustomHttpProtocol | ||||||
|  |     } | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/1', | ||||||
|  |                                             server_kwargs=server_kwargs) | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.text == 'OK' | ||||||
| @@ -1,51 +1,86 @@ | |||||||
|  | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.exceptions import InvalidUsage, ServerError, NotFound | from sanic.exceptions import InvalidUsage, ServerError, NotFound | ||||||
| from sanic.utils import sanic_endpoint_test | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # |  | ||||||
| #  GET |  | ||||||
| # ------------------------------------------------------------ # |  | ||||||
|  |  | ||||||
| exception_app = Sanic('test_exceptions') | class SanicExceptionTestException(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_app.route('/') | @pytest.fixture(scope='module') | ||||||
| def handler(request): | def exception_app(): | ||||||
|     return text('OK') |     app = Sanic('test_exceptions') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     def handler(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     @app.route('/error') | ||||||
|  |     def handler_error(request): | ||||||
|  |         raise ServerError("OK") | ||||||
|  |  | ||||||
|  |     @app.route('/404') | ||||||
|  |     def handler_404(request): | ||||||
|  |         raise NotFound("OK") | ||||||
|  |  | ||||||
|  |     @app.route('/invalid') | ||||||
|  |     def handler_invalid(request): | ||||||
|  |         raise InvalidUsage("OK") | ||||||
|  |  | ||||||
|  |     @app.route('/divide_by_zero') | ||||||
|  |     def handle_unhandled_exception(request): | ||||||
|  |         1 / 0 | ||||||
|  |  | ||||||
|  |     @app.route('/error_in_error_handler_handler') | ||||||
|  |     def custom_error_handler(request): | ||||||
|  |         raise SanicExceptionTestException('Dummy message!') | ||||||
|  |  | ||||||
|  |     @app.exception(SanicExceptionTestException) | ||||||
|  |     def error_in_error_handler_handler(request, exception): | ||||||
|  |         1 / 0 | ||||||
|  |  | ||||||
|  |     return app | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_app.route('/error') | def test_no_exception(exception_app): | ||||||
| def handler_error(request): |     """Test that a route works without an exception""" | ||||||
|     raise ServerError("OK") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_app.route('/404') |  | ||||||
| def handler_404(request): |  | ||||||
|     raise NotFound("OK") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_app.route('/invalid') |  | ||||||
| def handler_invalid(request): |  | ||||||
|     raise InvalidUsage("OK") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_no_exception(): |  | ||||||
|     request, response = sanic_endpoint_test(exception_app) |     request, response = sanic_endpoint_test(exception_app) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.text == 'OK' |     assert response.text == 'OK' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_server_error_exception(): | def test_server_error_exception(exception_app): | ||||||
|  |     """Test the built-in ServerError exception works""" | ||||||
|     request, response = sanic_endpoint_test(exception_app, uri='/error') |     request, response = sanic_endpoint_test(exception_app, uri='/error') | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_invalid_usage_exception(): | def test_invalid_usage_exception(exception_app): | ||||||
|  |     """Test the built-in InvalidUsage exception works""" | ||||||
|     request, response = sanic_endpoint_test(exception_app, uri='/invalid') |     request, response = sanic_endpoint_test(exception_app, uri='/invalid') | ||||||
|     assert response.status == 400 |     assert response.status == 400 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_not_found_exception(): | def test_not_found_exception(exception_app): | ||||||
|  |     """Test the built-in NotFound exception works""" | ||||||
|     request, response = sanic_endpoint_test(exception_app, uri='/404') |     request, response = sanic_endpoint_test(exception_app, uri='/404') | ||||||
|     assert response.status == 404 |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_handled_unhandled_exception(exception_app): | ||||||
|  |     """Test that an exception not built into sanic is handled""" | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         exception_app, uri='/divide_by_zero') | ||||||
|  |     assert response.status == 500 | ||||||
|  |     assert response.body == b'An error occurred while generating the response' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_exception_in_exception_handler(exception_app): | ||||||
|  |     """Test that an exception thrown in an error handler is handled""" | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         exception_app, uri='/error_in_error_handler_handler') | ||||||
|  |     assert response.status == 500 | ||||||
|  |     assert response.body == b'An error occurred while handling an error' | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								tests/test_logging.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								tests/test_logging.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | import asyncio | ||||||
|  | from sanic.response import text | ||||||
|  | from sanic import Sanic | ||||||
|  | from io import StringIO | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | logging_format = '''module: %(module)s; \ | ||||||
|  | function: %(funcName)s(); \ | ||||||
|  | message: %(message)s''' | ||||||
|  |  | ||||||
|  | def test_log(): | ||||||
|  |     log_stream = StringIO() | ||||||
|  |     for handler in logging.root.handlers[:]: | ||||||
|  |             logging.root.removeHandler(handler) | ||||||
|  |     logging.basicConfig( | ||||||
|  |         format=logging_format, | ||||||
|  |         level=logging.DEBUG, | ||||||
|  |         stream=log_stream | ||||||
|  |     ) | ||||||
|  |     log = logging.getLogger() | ||||||
|  |     app = Sanic('test_logging', logger=True) | ||||||
|  |     @app.route('/') | ||||||
|  |     def handler(request): | ||||||
|  |         log.info('hello world') | ||||||
|  |         return text('hello') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |     log_text = log_stream.getvalue().strip().split('\n')[-3] | ||||||
|  |     assert log_text == "module: test_logging; function: handler(); message: hello world" | ||||||
|  |  | ||||||
|  | if __name__ =="__main__": | ||||||
|  |     test_log() | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| from multiprocessing import Array, Event, Process | from multiprocessing import Array, Event, Process | ||||||
| from time import sleep | from time import sleep, time | ||||||
| from ujson import loads as json_loads | from ujson import loads as json_loads | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic.response import json | ||||||
| from sanic.utils import local_request, HOST, PORT | from sanic.utils import local_request, HOST, PORT | ||||||
| @@ -13,8 +15,9 @@ from sanic.utils import local_request, HOST, PORT | |||||||
|  |  | ||||||
| # TODO: Figure out why this freezes on pytest but not when | # TODO: Figure out why this freezes on pytest but not when | ||||||
| # executed via interpreter | # executed via interpreter | ||||||
|  | @pytest.mark.skip( | ||||||
| def skip_test_multiprocessing(): |     reason="Freezes with pytest not on interpreter") | ||||||
|  | def test_multiprocessing(): | ||||||
|     app = Sanic('test_json') |     app = Sanic('test_json') | ||||||
|  |  | ||||||
|     response = Array('c', 50) |     response = Array('c', 50) | ||||||
| @@ -51,3 +54,28 @@ def skip_test_multiprocessing(): | |||||||
|         raise ValueError("Expected JSON response but got '{}'".format(response)) |         raise ValueError("Expected JSON response but got '{}'".format(response)) | ||||||
|  |  | ||||||
|     assert results.get('test') == True |     assert results.get('test') == True | ||||||
|  |  | ||||||
|  | @pytest.mark.skip( | ||||||
|  |     reason="Freezes with pytest not on interpreter") | ||||||
|  | def test_drain_connections(): | ||||||
|  |     app = Sanic('test_json') | ||||||
|  |  | ||||||
|  |     @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', '/') | ||||||
|  |         stop_event.set() | ||||||
|  |  | ||||||
|  |     start = time() | ||||||
|  |     app.serve_multiple({ | ||||||
|  |         'host': HOST, | ||||||
|  |         'port': PORT, | ||||||
|  |         'after_start': after_start, | ||||||
|  |         'request_handler': app.handle_request, | ||||||
|  |     }, workers=2, stop_event=stop_event) | ||||||
|  |     end = time() | ||||||
|  |  | ||||||
|  |     assert end - start < 0.05 | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								tests/test_payload_too_large.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								tests/test_payload_too_large.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import text | ||||||
|  | from sanic.exceptions import PayloadTooLarge | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  | data_received_app = Sanic('data_received') | ||||||
|  | data_received_app.config.REQUEST_MAX_SIZE = 1 | ||||||
|  | data_received_default_app = Sanic('data_received_default') | ||||||
|  | data_received_default_app.config.REQUEST_MAX_SIZE = 1 | ||||||
|  | on_header_default_app = Sanic('on_header') | ||||||
|  | on_header_default_app.config.REQUEST_MAX_SIZE = 500 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @data_received_app.route('/1') | ||||||
|  | async def handler1(request): | ||||||
|  |     return text('OK') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @data_received_app.exception(PayloadTooLarge) | ||||||
|  | def handler_exception(request, exception): | ||||||
|  |     return text('Payload Too Large from error_handler.', 413) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_payload_too_large_from_error_handler(): | ||||||
|  |     response = sanic_endpoint_test( | ||||||
|  |         data_received_app, uri='/1', gather_request=False) | ||||||
|  |     assert response.status == 413 | ||||||
|  |     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(): | ||||||
|  |     response = sanic_endpoint_test( | ||||||
|  |         data_received_default_app, uri='/1', gather_request=False) | ||||||
|  |     assert response.status == 413 | ||||||
|  |     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(): | ||||||
|  |     data = 'a' * 1000 | ||||||
|  |     response = sanic_endpoint_test( | ||||||
|  |         on_header_default_app, method='post', uri='/1', | ||||||
|  |         gather_request=False, data=data) | ||||||
|  |     assert response.status == 413 | ||||||
|  |     assert response.text == 'Error: Payload Too Large' | ||||||
							
								
								
									
										24
									
								
								tests/test_request_data.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tests/test_request_data.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import json | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  | from ujson import loads | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_storage(): | ||||||
|  |     app = Sanic('test_text') | ||||||
|  |  | ||||||
|  |     @app.middleware('request') | ||||||
|  |     def store(request): | ||||||
|  |         request['user'] = 'sanic' | ||||||
|  |         request['sidekick'] = 'tails' | ||||||
|  |         del request['sidekick'] | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     def handler(request): | ||||||
|  |         return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') }) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|  |     response_json = loads(response.text) | ||||||
|  |     assert response_json['user'] == 'sanic' | ||||||
|  |     assert response_json.get('sidekick') is None | ||||||
							
								
								
									
										40
									
								
								tests/test_request_timeout.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								tests/test_request_timeout.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | import asyncio | ||||||
|  | from sanic.response import text | ||||||
|  | from sanic.exceptions import RequestTimeout | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  | from sanic.config import Config | ||||||
|  |  | ||||||
|  | Config.REQUEST_TIMEOUT = 1 | ||||||
|  | request_timeout_app = Sanic('test_request_timeout') | ||||||
|  | request_timeout_default_app = Sanic('test_request_timeout_default') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @request_timeout_app.route('/1') | ||||||
|  | async def handler_1(request): | ||||||
|  |     await asyncio.sleep(2) | ||||||
|  |     return text('OK') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @request_timeout_app.exception(RequestTimeout) | ||||||
|  | def handler_exception(request, exception): | ||||||
|  |     return text('Request Timeout from error_handler.', 408) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_server_error_request_timeout(): | ||||||
|  |     request, response = sanic_endpoint_test(request_timeout_app, uri='/1') | ||||||
|  |     assert response.status == 408 | ||||||
|  |     assert response.text == 'Request Timeout from error_handler.' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @request_timeout_default_app.route('/1') | ||||||
|  | async def handler_2(request): | ||||||
|  |     await asyncio.sleep(2) | ||||||
|  |     return text('OK') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_default_server_error_request_timeout(): | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         request_timeout_default_app, uri='/1') | ||||||
|  |     assert response.status == 408 | ||||||
|  |     assert response.text == 'Error: Request Timeout' | ||||||
| @@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json, text | from sanic.response import json, text | ||||||
| from sanic.utils import sanic_endpoint_test | from sanic.utils import sanic_endpoint_test | ||||||
|  | from sanic.exceptions import ServerError | ||||||
|  |  | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
| @@ -32,6 +33,47 @@ def test_text(): | |||||||
|     assert response.text == 'Hello' |     assert response.text == 'Hello' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_headers(): | ||||||
|  |     app = Sanic('test_text') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         headers = {"spam": "great"} | ||||||
|  |         return text('Hello', headers=headers) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|  |     assert response.headers.get('spam') == 'great' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_non_str_headers(): | ||||||
|  |     app = Sanic('test_text') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         headers = {"answer": 42} | ||||||
|  |         return text('Hello', headers=headers) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|  |     assert response.headers.get('answer') == '42' | ||||||
|  |      | ||||||
|  | def test_invalid_response(): | ||||||
|  |     app = Sanic('test_invalid_response') | ||||||
|  |  | ||||||
|  |     @app.exception(ServerError) | ||||||
|  |     def handler_exception(request, exception): | ||||||
|  |         return text('Internal Server Error.', 500) | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         return 'This should fail' | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |     assert response.status == 500 | ||||||
|  |     assert response.text == "Internal Server Error." | ||||||
|  |      | ||||||
|  |      | ||||||
| def test_json(): | def test_json(): | ||||||
|     app = Sanic('test_json') |     app = Sanic('test_json') | ||||||
|  |  | ||||||
| @@ -49,6 +91,19 @@ def test_json(): | |||||||
|     assert results.get('test') == True |     assert results.get('test') == True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_invalid_json(): | ||||||
|  |     app = Sanic('test_json') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         return json(request.json()) | ||||||
|  |  | ||||||
|  |     data = "I am not json" | ||||||
|  |     request, response = sanic_endpoint_test(app, data=data) | ||||||
|  |  | ||||||
|  |     assert response.status == 400 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_query_string(): | def test_query_string(): | ||||||
|     app = Sanic('test_query_string') |     app = Sanic('test_query_string') | ||||||
|  |  | ||||||
| @@ -56,12 +111,30 @@ def test_query_string(): | |||||||
|     async def handler(request): |     async def handler(request): | ||||||
|         return text('OK') |         return text('OK') | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")]) |     request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")]) | ||||||
|  |  | ||||||
|     assert request.args.get('test1') == '1' |     assert request.args.get('test1') == '1' | ||||||
|     assert request.args.get('test2') == 'false' |     assert request.args.get('test2') == 'false' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_token(): | ||||||
|  |     app = Sanic('test_post_token') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     # uuid4 generated token. | ||||||
|  |     token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' | ||||||
|  |     headers = { | ||||||
|  |         'content-type': 'application/json', | ||||||
|  |         'Authorization': 'Token {}'.format(token) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, headers=headers) | ||||||
|  |  | ||||||
|  |     assert request.token == token | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
| #  POST | #  POST | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								tests/test_response.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/test_response.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | from random import choice | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import HTTPResponse | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_response_body_not_a_string(): | ||||||
|  |     """Test when a response body sent from the application is not a string""" | ||||||
|  |     app = Sanic('response_body_not_a_string') | ||||||
|  |     random_num = choice(range(1000)) | ||||||
|  |  | ||||||
|  |     @app.route('/hello') | ||||||
|  |     async def hello_route(request): | ||||||
|  |         return HTTPResponse(body=random_num) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/hello') | ||||||
|  |     assert response.text == str(random_num) | ||||||
| @@ -2,7 +2,7 @@ import pytest | |||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.router import RouteExists | from sanic.router import RouteExists, RouteDoesNotExist | ||||||
| from sanic.utils import sanic_endpoint_test | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -84,7 +84,7 @@ def test_dynamic_route_int(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_dynamic_route_number(): | def test_dynamic_route_number(): | ||||||
|     app = Sanic('test_dynamic_route_int') |     app = Sanic('test_dynamic_route_number') | ||||||
|  |  | ||||||
|     results = [] |     results = [] | ||||||
|  |  | ||||||
| @@ -105,7 +105,7 @@ def test_dynamic_route_number(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_dynamic_route_regex(): | def test_dynamic_route_regex(): | ||||||
|     app = Sanic('test_dynamic_route_int') |     app = Sanic('test_dynamic_route_regex') | ||||||
|  |  | ||||||
|     @app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>') |     @app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>') | ||||||
|     async def handler(request, folder_id): |     async def handler(request, folder_id): | ||||||
| @@ -145,7 +145,7 @@ def test_dynamic_route_unhashable(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_route_duplicate(): | def test_route_duplicate(): | ||||||
|     app = Sanic('test_dynamic_route') |     app = Sanic('test_route_duplicate') | ||||||
|  |  | ||||||
|     with pytest.raises(RouteExists): |     with pytest.raises(RouteExists): | ||||||
|         @app.route('/test') |         @app.route('/test') | ||||||
| @@ -178,3 +178,288 @@ def test_method_not_allowed(): | |||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, method='post', uri='/test') |     request, response = sanic_endpoint_test(app, method='post', uri='/test') | ||||||
|     assert response.status == 405 |     assert response.status == 405 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_static_add_route(): | ||||||
|  |     app = Sanic('test_static_add_route') | ||||||
|  |  | ||||||
|  |     async def handler1(request): | ||||||
|  |         return text('OK1') | ||||||
|  |  | ||||||
|  |     async def handler2(request): | ||||||
|  |         return text('OK2') | ||||||
|  |  | ||||||
|  |     app.add_route(handler1, '/test') | ||||||
|  |     app.add_route(handler2, '/test2') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.text == 'OK1' | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test2') | ||||||
|  |     assert response.text == 'OK2' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route(): | ||||||
|  |     app = Sanic('test_dynamic_add_route') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     async def handler(request, name): | ||||||
|  |         results.append(name) | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<name>') | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test123') | ||||||
|  |  | ||||||
|  |     assert response.text == 'OK' | ||||||
|  |     assert results[0] == 'test123' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_string(): | ||||||
|  |     app = Sanic('test_dynamic_add_route_string') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     async def handler(request, name): | ||||||
|  |         results.append(name) | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<name:string>') | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test123') | ||||||
|  |  | ||||||
|  |     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_add_route_int(): | ||||||
|  |     app = Sanic('test_dynamic_add_route_int') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     async def handler(request, folder_id): | ||||||
|  |         results.append(folder_id) | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<folder_id:int>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/12345') | ||||||
|  |     assert response.text == 'OK' | ||||||
|  |     assert type(results[0]) is int | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/asdf') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_number(): | ||||||
|  |     app = Sanic('test_dynamic_add_route_number') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     async def handler(request, weight): | ||||||
|  |         results.append(weight) | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/weight/<weight:number>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/weight/12345') | ||||||
|  |     assert response.text == 'OK' | ||||||
|  |     assert type(results[0]) is float | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/weight/1234.56') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/weight/1234-56') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_regex(): | ||||||
|  |     app = Sanic('test_dynamic_route_int') | ||||||
|  |  | ||||||
|  |     async def handler(request, folder_id): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test1') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test-123') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_unhashable(): | ||||||
|  |     app = Sanic('test_dynamic_add_route_unhashable') | ||||||
|  |  | ||||||
|  |     async def handler(request, unhashable): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_add_route_duplicate(): | ||||||
|  |     app = Sanic('test_add_route_duplicate') | ||||||
|  |  | ||||||
|  |     with pytest.raises(RouteExists): | ||||||
|  |         async def handler1(request): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         async def handler2(request): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         app.add_route(handler1, '/test') | ||||||
|  |         app.add_route(handler2, '/test') | ||||||
|  |  | ||||||
|  |     with pytest.raises(RouteExists): | ||||||
|  |         async def handler1(request, dynamic): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         async def handler2(request, dynamic): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         app.add_route(handler1, '/test/<dynamic>/') | ||||||
|  |         app.add_route(handler2, '/test/<dynamic>/') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_add_route_method_not_allowed(): | ||||||
|  |     app = Sanic('test_add_route_method_not_allowed') | ||||||
|  |  | ||||||
|  |     async def handler(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/test', methods=['GET']) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, method='post', uri='/test') | ||||||
|  |     assert response.status == 405 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_static_route(): | ||||||
|  |     app = Sanic('test_remove_static_route') | ||||||
|  |  | ||||||
|  |     async def handler1(request): | ||||||
|  |         return text('OK1') | ||||||
|  |  | ||||||
|  |     async def handler2(request): | ||||||
|  |         return text('OK2') | ||||||
|  |  | ||||||
|  |     app.add_route(handler1, '/test') | ||||||
|  |     app.add_route(handler2, '/test2') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test2') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/test') | ||||||
|  |     app.remove_route('/test2') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test2') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_dynamic_route(): | ||||||
|  |     app = Sanic('test_remove_dynamic_route') | ||||||
|  |  | ||||||
|  |     async def handler(request, name): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<name>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test123') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/folder/<name>') | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test123') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_inexistent_route(): | ||||||
|  |     app = Sanic('test_remove_inexistent_route') | ||||||
|  |  | ||||||
|  |     with pytest.raises(RouteDoesNotExist): | ||||||
|  |         app.remove_route('/test') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_unhashable_route(): | ||||||
|  |     app = Sanic('test_remove_unhashable_route') | ||||||
|  |  | ||||||
|  |     async def handler(request, unhashable): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/end/') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_route_without_clean_cache(): | ||||||
|  |     app = Sanic('test_remove_static_route') | ||||||
|  |  | ||||||
|  |     async def handler(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/test') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/test', clean_cache=True) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/test') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/test', clean_cache=False) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|   | |||||||
| @@ -1,30 +1,62 @@ | |||||||
| import inspect | import inspect | ||||||
| import os | import os | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.utils import sanic_endpoint_test | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
| def test_static_file(): |  | ||||||
|     current_file = inspect.getfile(inspect.currentframe()) |  | ||||||
|     with open(current_file, 'rb') as file: |  | ||||||
|         current_file_contents = file.read() |  | ||||||
|  |  | ||||||
|  | @pytest.fixture(scope='module') | ||||||
|  | def static_file_directory(): | ||||||
|  |     """The static directory to serve""" | ||||||
|  |     current_file = inspect.getfile(inspect.currentframe()) | ||||||
|  |     current_directory = os.path.dirname(os.path.abspath(current_file)) | ||||||
|  |     static_directory = os.path.join(current_directory, 'static') | ||||||
|  |     return static_directory | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture(scope='module') | ||||||
|  | def static_file_path(static_file_directory): | ||||||
|  |     """The path to the static file that we want to serve""" | ||||||
|  |     return os.path.join(static_file_directory, 'test.file') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture(scope='module') | ||||||
|  | def static_file_content(static_file_path): | ||||||
|  |     """The content of the static file to check""" | ||||||
|  |     with open(static_file_path, 'rb') as file: | ||||||
|  |         return file.read() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_static_file(static_file_path, static_file_content): | ||||||
|     app = Sanic('test_static') |     app = Sanic('test_static') | ||||||
|     app.static('/testing.file', current_file) |     app.static('/testing.file', static_file_path) | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, uri='/testing.file') |     request, response = sanic_endpoint_test(app, uri='/testing.file') | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == current_file_contents |     assert response.body == static_file_content | ||||||
|  |  | ||||||
| def test_static_directory(): |  | ||||||
|     current_file = inspect.getfile(inspect.currentframe()) | def test_static_directory( | ||||||
|     current_directory = os.path.dirname(os.path.abspath(current_file)) |         static_file_directory, static_file_path, static_file_content): | ||||||
|     with open(current_file, 'rb') as file: |  | ||||||
|         current_file_contents = file.read() |  | ||||||
|  |  | ||||||
|     app = Sanic('test_static') |     app = Sanic('test_static') | ||||||
|     app.static('/dir', current_directory) |     app.static('/dir', static_file_directory) | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') |     request, response = sanic_endpoint_test(app, uri='/dir/test.file') | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == current_file_contents |     assert response.body == static_file_content | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_static_url_decode_file(static_file_directory): | ||||||
|  |     decode_me_path = os.path.join(static_file_directory, 'decode me.txt') | ||||||
|  |     with open(decode_me_path, 'rb') as file: | ||||||
|  |         decode_me_contents = file.read() | ||||||
|  |  | ||||||
|  |     app = Sanic('test_static') | ||||||
|  |     app.static('/dir', static_file_directory) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt') | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.body == decode_me_contents | ||||||
|   | |||||||
							
								
								
									
										196
									
								
								tests/test_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								tests/test_views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import text, HTTPResponse | ||||||
|  | from sanic.views import HTTPMethodView | ||||||
|  | from sanic.blueprints import Blueprint | ||||||
|  | from sanic.request import Request | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_methods(): | ||||||
|  |     app = Sanic('test_methods') | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             return text('I am get method') | ||||||
|  |  | ||||||
|  |         def post(self, request): | ||||||
|  |             return text('I am post method') | ||||||
|  |  | ||||||
|  |         def put(self, request): | ||||||
|  |             return text('I am put method') | ||||||
|  |  | ||||||
|  |         def patch(self, request): | ||||||
|  |             return text('I am patch method') | ||||||
|  |  | ||||||
|  |         def delete(self, request): | ||||||
|  |             return text('I am delete method') | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, method="get") | ||||||
|  |     assert response.text == 'I am get method' | ||||||
|  |     request, response = sanic_endpoint_test(app, method="post") | ||||||
|  |     assert response.text == 'I am post method' | ||||||
|  |     request, response = sanic_endpoint_test(app, method="put") | ||||||
|  |     assert response.text == 'I am put method' | ||||||
|  |     request, response = sanic_endpoint_test(app, method="patch") | ||||||
|  |     assert response.text == 'I am patch method' | ||||||
|  |     request, response = sanic_endpoint_test(app, method="delete") | ||||||
|  |     assert response.text == 'I am delete method' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_unexisting_methods(): | ||||||
|  |     app = Sanic('test_unexisting_methods') | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             return text('I am get method') | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |     request, response = sanic_endpoint_test(app, method="get") | ||||||
|  |     assert response.text == 'I am get method' | ||||||
|  |     request, response = sanic_endpoint_test(app, method="post") | ||||||
|  |     assert response.text == 'Error: Method POST not allowed for URL /' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_argument_methods(): | ||||||
|  |     app = Sanic('test_argument_methods') | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |         def get(self, request, my_param_here): | ||||||
|  |             return text('I am get method with %s' % my_param_here) | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/<my_param_here>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test123') | ||||||
|  |  | ||||||
|  |     assert response.text == 'I am get method with test123' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_with_bp(): | ||||||
|  |     app = Sanic('test_with_bp') | ||||||
|  |     bp = Blueprint('test_text') | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             return text('I am get method') | ||||||
|  |  | ||||||
|  |     bp.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|  |     app.blueprint(bp) | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|  |     assert response.text == 'I am get method' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_with_bp_with_url_prefix(): | ||||||
|  |     app = Sanic('test_with_bp_with_url_prefix') | ||||||
|  |     bp = Blueprint('test_text', url_prefix='/test1') | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             return text('I am get method') | ||||||
|  |  | ||||||
|  |     bp.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|  |     app.blueprint(bp) | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||||
|  |  | ||||||
|  |     assert response.text == 'I am get method' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_with_middleware(): | ||||||
|  |     app = Sanic('test_with_middleware') | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             return text('I am get method') | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     @app.middleware | ||||||
|  |     async def handler(request): | ||||||
|  |         results.append(request) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|  |     assert response.text == 'I am get method' | ||||||
|  |     assert type(results[0]) is Request | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_with_middleware_response(): | ||||||
|  |     app = Sanic('test_with_middleware_response') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     @app.middleware('request') | ||||||
|  |     async def process_response(request): | ||||||
|  |         results.append(request) | ||||||
|  |  | ||||||
|  |     @app.middleware('response') | ||||||
|  |     async def process_response(request, response): | ||||||
|  |         results.append(request) | ||||||
|  |         results.append(response) | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             return text('I am get method') | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|  |     assert response.text == 'I am get method' | ||||||
|  |     assert type(results[0]) is Request | ||||||
|  |     assert type(results[1]) is Request | ||||||
|  |     assert issubclass(type(results[2]), HTTPResponse) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_with_custom_class_methods(): | ||||||
|  |     app = Sanic('test_with_custom_class_methods') | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |         global_var = 0 | ||||||
|  |  | ||||||
|  |         def _iternal_method(self): | ||||||
|  |             self.global_var += 10 | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             self._iternal_method() | ||||||
|  |             return text('I am get method and global var is {}'.format(self.global_var)) | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |     request, response = sanic_endpoint_test(app, method="get") | ||||||
|  |     assert response.text == 'I am get method and global var is 10' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_with_decorator(): | ||||||
|  |     app = Sanic('test_with_decorator') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     def stupid_decorator(view): | ||||||
|  |         def decorator(*args, **kwargs): | ||||||
|  |             results.append(1) | ||||||
|  |             return view(*args, **kwargs) | ||||||
|  |         return decorator | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |         decorators = [stupid_decorator] | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             return text('I am get method') | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |     request, response = sanic_endpoint_test(app, method="get") | ||||||
|  |     assert response.text == 'I am get method' | ||||||
|  |     assert results[0] == 1 | ||||||
							
								
								
									
										59
									
								
								tests/tests_server_events.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								tests/tests_server_events.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | from io import StringIO | ||||||
|  | from random import choice | ||||||
|  | from string import ascii_letters | ||||||
|  | import signal | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  |  | ||||||
|  | AVAILABLE_LISTENERS = [ | ||||||
|  |     'before_start', | ||||||
|  |     'after_start', | ||||||
|  |     'before_stop', | ||||||
|  |     'after_stop' | ||||||
|  | ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_listener(listener_name, in_list): | ||||||
|  |     async def _listener(app, loop): | ||||||
|  |         print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name)) | ||||||
|  |         in_list.insert(0, app.name + listener_name) | ||||||
|  |     return _listener | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def start_stop_app(random_name_app, **run_kwargs): | ||||||
|  |  | ||||||
|  |     def stop_on_alarm(signum, frame): | ||||||
|  |         raise KeyboardInterrupt('SIGINT for sanic to stop gracefully') | ||||||
|  |  | ||||||
|  |     signal.signal(signal.SIGALRM, stop_on_alarm) | ||||||
|  |     signal.alarm(1) | ||||||
|  |     try: | ||||||
|  |         random_name_app.run(**run_kwargs) | ||||||
|  |     except KeyboardInterrupt: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS) | ||||||
|  | def test_single_listener(listener_name): | ||||||
|  |     """Test that listeners on their own work""" | ||||||
|  |     random_name_app = Sanic(''.join( | ||||||
|  |         [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) | ||||||
|  |     output = list() | ||||||
|  |     start_stop_app( | ||||||
|  |         random_name_app, | ||||||
|  |         **{listener_name: create_listener(listener_name, output)}) | ||||||
|  |     assert random_name_app.name + listener_name == output.pop() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_all_listeners(): | ||||||
|  |     random_name_app = Sanic(''.join( | ||||||
|  |         [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) | ||||||
|  |     output = list() | ||||||
|  |     start_stop_app( | ||||||
|  |         random_name_app, | ||||||
|  |         **{listener_name: create_listener(listener_name, output) | ||||||
|  |            for listener_name in AVAILABLE_LISTENERS}) | ||||||
|  |     for listener_name in AVAILABLE_LISTENERS: | ||||||
|  |         assert random_name_app.name + listener_name == output.pop() | ||||||
							
								
								
									
										33
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,34 +1,25 @@ | |||||||
| [tox] | [tox] | ||||||
|  |  | ||||||
| envlist = py35, report | envlist = py35, py36, flake8 | ||||||
|  |  | ||||||
|  | [travis] | ||||||
|  |  | ||||||
|  | python = | ||||||
|  |     3.5: py35, flake8 | ||||||
|  |     3.6: py36, flake8 | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
|  |  | ||||||
| deps = | deps = | ||||||
|     aiohttp |     aiohttp | ||||||
|     pytest |     pytest | ||||||
|     # pytest-cov |  | ||||||
|     coverage |  | ||||||
|  |  | ||||||
| commands = | commands = | ||||||
|     coverage run -m pytest tests {posargs} |     pytest tests {posargs} | ||||||
|     mv .coverage .coverage.{envname} |  | ||||||
|  |  | ||||||
| basepython: | [testenv:flake8] | ||||||
|     py35: python3.5 | deps = | ||||||
|  |     flake8 | ||||||
| whitelist_externals = |  | ||||||
|     coverage |  | ||||||
|     mv |  | ||||||
|     echo |  | ||||||
|  |  | ||||||
| [testenv:report] |  | ||||||
|  |  | ||||||
| commands = | commands = | ||||||
|     coverage combine |     flake8 sanic | ||||||
|     coverage report |  | ||||||
|     coverage html |  | ||||||
|     echo "Open file://{toxinidir}/coverage/index.html" |  | ||||||
|  |  | ||||||
| basepython = |  | ||||||
|     python3.5 |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Marcin Baran
					Marcin Baran