Compare commits
	
		
			147 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 6cf3754051 | ||
|   | cf7616ebe5 | ||
|   | 5b7964f8b6 | ||
|   | f1f38c24da | ||
|   | 67a50becb0 | ||
|   | d7e94473f3 | ||
|   | 184c896f41 | ||
|   | 8be849cc40 | ||
|   | 275851a755 | ||
|   | 16182472fa | ||
|   | 29f3c22fed | ||
|   | a116666d55 | ||
|   | c2622511ce | ||
|   | 50243037eb | ||
|   | 74f305cfb7 | ||
|   | 75990fbaf4 | ||
|   | cc982c5a61 | ||
|   | 2f0a582aa7 | ||
|   | 665881471d | ||
|   | cd17a42234 | ||
|   | 8e19b5938c | ||
|   | 9e208ab744 | ||
|   | 94bd9702e5 | ||
|   | 3add40625d | ||
|   | 5afae986a0 | ||
|   | f091d82bad | ||
|   | 5c1ef2c1cf | ||
|   | 8411255700 | ||
|   | ef9d8710f5 | ||
|   | 75fc9f91b9 | ||
|   | 545d9eb59b | ||
|   | a9b67c3028 | ||
|   | 35e79f3985 | ||
|   | 435d5585e9 | ||
|   | ddfb7f2861 | ||
|   | 6c806549ae | ||
|   | 8957e4ec25 | ||
|   | 2003eceba1 | ||
|   | 8fc1462d11 | ||
|   | 93b45e9598 | ||
|   | a3a14cdab2 | ||
|   | 9ba2f99ea2 | ||
|   | f9db796a6e | ||
|   | 94c7aaf7f8 | ||
|   | 6ef6d9a905 | ||
|   | b44e9baaec | ||
|   | f9176bfdea | ||
|   | 721044b378 | ||
|   | 154f8570f0 | ||
|   | 0464d31a9c | ||
|   | e3453553e1 | ||
|   | 6abaa78f9e | ||
|   | 457507d8dc | ||
|   | 3ea1a80496 | ||
|   | fac4bca4f4 | ||
|   | 662e0c9965 | ||
|   | 80af9e6d76 | ||
|   | 9b466db5c9 | ||
|   | c34427690a | ||
|   | d8a974bb4f | ||
|   | 98b08676e2 | ||
|   | 39f3a63ced | ||
|   | 89e2084489 | ||
|   | 70c56b7db3 | ||
|   | 209b763302 | ||
|   | 190b7a6076 | ||
|   | cce47a633a | ||
|   | ec2330c42b | ||
|   | 0c215685f2 | ||
|   | d86ac5e3e0 | ||
|   | ee89b6ad03 | ||
|   | a5e6d6d2e8 | ||
|   | 1eea1f5485 | ||
|   | da4567eea5 | ||
|   | 9010a6573f | ||
|   | d8e480ab48 | ||
|   | 0bd61f6a57 | ||
|   | c01cbb3a8c | ||
|   | 0ca5c4eeff | ||
|   | 47927608b2 | ||
|   | 13808bf282 | ||
|   | c3c7964e2e | ||
|   | fca0221d91 | ||
|   | 9f2d73e2f1 | ||
|   | fc19f2ea34 | ||
|   | aa0f15fbb2 | ||
|   | 93f50b8ef7 | ||
|   | 7b85843363 | ||
|   | f7f578ed44 | ||
|   | de92603ccf | ||
|   | d02fffb6b8 | ||
|   | 922c96e3c1 | ||
|   | 993627ec44 | ||
|   | 01681599ff | ||
|   | 3ce6434532 | ||
|   | a97e554f8f | ||
|   | fd5a79a685 | ||
|   | 635921adc7 | ||
|   | 9eb4cecbc1 | ||
|   | 879b9a4a15 | ||
|   | 8be4dc8fb5 | ||
|   | f16ea20de5 | ||
|   | c51b14856e | ||
|   | 88ee71c425 | ||
|   | edb12da154 | ||
|   | d9f6846c76 | ||
|   | 9e0747db15 | ||
|   | ae3d33ad58 | ||
|   | edb25f799d | ||
|   | 0822674f70 | ||
|   | 49d004736a | ||
|   | 695f8733bb | ||
|   | b51af7f4bf | ||
|   | 28ce2447ef | ||
|   | 42e3a50274 | ||
|   | 8ebc92c236 | ||
|   | b92e46df40 | ||
|   | be5588d5d8 | ||
|   | 0d9fb2f927 | ||
|   | 0e9819fba1 | ||
|   | aaee40aabd | ||
|   | 5efe51b661 | ||
|   | 50f63142db | ||
|   | 1b65b2e0c6 | ||
|   | ce8742c605 | ||
|   | 01a013b48a | ||
|   | 3a2eeb9709 | ||
|   | 1271c7d958 | ||
|   | 0032f525ce | ||
|   | df2f91b82f | ||
|   | 3a1ef6bef2 | ||
|   | 28488075b9 | ||
|   | 3cd3b2d9b7 | ||
|   | 3d88818841 | ||
|   | b74cf65eca | ||
|   | 80fcacaf8b | ||
|   | 96fcd8443f | ||
|   | 707c55fbe7 | ||
|   | c44b5551bc | ||
|   | bd28da0abc | ||
|   | 410299f5a1 | ||
|   | f3fc958a0c | ||
|   | 47b417db28 | ||
|   | 5171cdd305 | ||
|   | 65950250d9 | ||
|   | 74ae0007d3 | ||
|   | f95fe4192b | 
| @@ -1,6 +1,7 @@ | ||||
| language: python | ||||
| python: | ||||
|   - '3.5' | ||||
|   - '3.6' | ||||
| install: | ||||
|   - pip install -r requirements.txt | ||||
|   - pip install -r requirements-dev.txt | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| Version 0.1 | ||||
| ----------- | ||||
|  - 0.1.6 (not released) | ||||
|  - 0.1.7 | ||||
|   - Reversed static url and directory arguments to meet spec | ||||
|  - 0.1.6 | ||||
|   - Static files | ||||
|   - Lazy Cookie Loading | ||||
|  - 0.1.5  | ||||
|   - Cookies | ||||
|   - Blueprint listeners and ordering | ||||
|   | ||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,5 +1,7 @@ | ||||
| # 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://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.response import json | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| app = Sanic() | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| 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 | ||||
| @@ -50,8 +56,10 @@ app.run(host="0.0.0.0", port=8000) | ||||
|  * [Middleware](docs/middleware.md) | ||||
|  * [Exceptions](docs/exceptions.md) | ||||
|  * [Blueprints](docs/blueprints.md) | ||||
|  * [Class Based Views](docs/class_based_views.md) | ||||
|  * [Cookies](docs/cookies.md) | ||||
|  * [Static Files](docs/static_files.md) | ||||
|  * [Testing](docs/testing.md) | ||||
|  * [Deploying](docs/deploying.md) | ||||
|  * [Contributing](docs/contributing.md) | ||||
|  * [License](LICENSE) | ||||
| @@ -70,7 +78,7 @@ app.run(host="0.0.0.0", port=8000) | ||||
|                      ▄▄▄▄▄ | ||||
|             ▀▀▀██████▄▄▄       _______________ | ||||
|           ▄▄▄▄▄  █████████▄  /                 \ | ||||
|          ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  |  | ||||
|          ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  | | ||||
|        ▀▀█████▄▄ ▀██████▄██ | _________________/ | ||||
|        ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/ | ||||
|             ▀▀▀▄  ▀▀███ ▀       ▄▄ | ||||
|   | ||||
							
								
								
									
										45
									
								
								docs/class_based_views.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								docs/class_based_views.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| # 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(), '/') | ||||
|  | ||||
| ``` | ||||
|  | ||||
| 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(), '/<name>') | ||||
|  | ||||
| ``` | ||||
| @@ -27,3 +27,23 @@ async def handler(request): | ||||
|  | ||||
| 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): | ||||
| 	return text('Folder - {}'.format(folder_id)) | ||||
|  | ||||
| async def handler1(request): | ||||
| 	return text('OK') | ||||
| app.add_route(handler1, '/test') | ||||
|  | ||||
| async def handler(request, name): | ||||
| 	return text('Folder - {}'.format(name)) | ||||
| app.add_route(handler, '/folder/<name>') | ||||
|  | ||||
| async def person_handler(request, name): | ||||
| 	return text('Person - {}'.format(name)) | ||||
| app.add_route(handler, '/person/<name:[A-z]>') | ||||
|  | ||||
| ``` | ||||
|   | ||||
| @@ -8,11 +8,11 @@ Both directories and files can be served by registering with static | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| # Serves files from the static folder to the URL /static | ||||
| app.static('./static', '/static') | ||||
| app.static('/static', './static') | ||||
|  | ||||
| # Serves the file /home/ubuntu/test.png when the URL /the_best.png | ||||
| # is requested | ||||
| app.static('/home/ubuntu/test.png', '/the_best.png') | ||||
| app.static('/the_best.png', '/home/ubuntu/test.png') | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000) | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										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'] | ||||
| ``` | ||||
							
								
								
									
										33
									
								
								examples/aiohttp_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								examples/aiohttp_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
|  | ||||
| import uvloop | ||||
| import aiohttp | ||||
|  | ||||
| #Create an event loop manually so that we can use it for both sanic & aiohttp | ||||
| loop = uvloop.new_event_loop() | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| async def fetch(session, url): | ||||
|     """ | ||||
|     Use session object to perform 'get' request on url | ||||
|     """ | ||||
|     async with session.get(url) as response: | ||||
|         return await response.json() | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| async def test(request): | ||||
|     """ | ||||
|     Download and serve example JSON | ||||
|     """ | ||||
|     url = "https://api.github.com/repos/channelcat/sanic" | ||||
|  | ||||
|     async with aiohttp.ClientSession(loop=loop) as session: | ||||
|         response = await fetch(session, url) | ||||
|         return json(response) | ||||
|  | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000, loop=loop) | ||||
|  | ||||
							
								
								
									
										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) | ||||
							
								
								
									
										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) | ||||
| @@ -2,6 +2,7 @@ httptools | ||||
| ujson | ||||
| uvloop | ||||
| aiohttp | ||||
| aiocache | ||||
| pytest | ||||
| coverage | ||||
| tox | ||||
| @@ -9,4 +10,5 @@ gunicorn | ||||
| bottle | ||||
| kyoukai | ||||
| falcon | ||||
| tornado | ||||
| tornado | ||||
| aiofiles | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| httptools | ||||
| ujson | ||||
| uvloop | ||||
| uvloop | ||||
| aiofiles | ||||
| multidict | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from .sanic import Sanic | ||||
| from .blueprints import Blueprint | ||||
|  | ||||
| __version__ = '0.1.6' | ||||
| __version__ = '0.1.9' | ||||
|  | ||||
| __all__ = ['Sanic', 'Blueprint'] | ||||
|   | ||||
| @@ -33,14 +33,14 @@ class BlueprintSetup: | ||||
|         """ | ||||
|         self.app.exception(*args, **kwargs)(handler) | ||||
|  | ||||
|     def add_static(self, file_or_directory, uri, *args, **kwargs): | ||||
|     def add_static(self, uri, file_or_directory, *args, **kwargs): | ||||
|         """ | ||||
|         Registers static files to sanic | ||||
|         """ | ||||
|         if self.url_prefix: | ||||
|             uri = self.url_prefix + uri | ||||
|  | ||||
|         self.app.static(file_or_directory, uri, *args, **kwargs) | ||||
|         self.app.static(uri, file_or_directory, *args, **kwargs) | ||||
|  | ||||
|     def add_middleware(self, middleware, *args, **kwargs): | ||||
|         """ | ||||
| @@ -91,6 +91,12 @@ class Blueprint: | ||||
|             return handler | ||||
|         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): | ||||
|         """ | ||||
|         """ | ||||
| @@ -109,8 +115,9 @@ class Blueprint: | ||||
|  | ||||
|         # Detect which way this was called, @middleware or @middleware('AT') | ||||
|         if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): | ||||
|             middleware = args[0] | ||||
|             args = [] | ||||
|             return register_middleware(args[0]) | ||||
|             return register_middleware(middleware) | ||||
|         else: | ||||
|             return register_middleware | ||||
|  | ||||
| @@ -122,8 +129,8 @@ class Blueprint: | ||||
|             return handler | ||||
|         return decorator | ||||
|  | ||||
|     def static(self, file_or_directory, uri, *args, **kwargs): | ||||
|     def static(self, uri, file_or_directory, *args, **kwargs): | ||||
|         """ | ||||
|         """ | ||||
|         self.record( | ||||
|             lambda s: s.add_static(file_or_directory, uri, *args, **kwargs)) | ||||
|             lambda s: s.add_static(uri, file_or_directory, *args, **kwargs)) | ||||
|   | ||||
| @@ -30,6 +30,7 @@ def _quote(str): | ||||
|     else: | ||||
|         return '"' + str.translate(_Translator) + '"' | ||||
|  | ||||
|  | ||||
| _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
|   | ||||
| @@ -30,6 +30,14 @@ class FileNotFound(NotFound): | ||||
|         self.relative_url = relative_url | ||||
|  | ||||
|  | ||||
| class RequestTimeout(SanicException): | ||||
|     status_code = 408 | ||||
|  | ||||
|  | ||||
| class PayloadTooLarge(SanicException): | ||||
|     status_code = 413 | ||||
|  | ||||
|  | ||||
| class Handler: | ||||
|     handlers = None | ||||
|  | ||||
|   | ||||
| @@ -4,10 +4,17 @@ from http.cookies import SimpleCookie | ||||
| from httptools import parse_url | ||||
| from urllib.parse import parse_qs | ||||
| from ujson import loads as json_loads | ||||
| from sanic.exceptions import InvalidUsage | ||||
|  | ||||
| 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): | ||||
|     """ | ||||
|     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) | ||||
|  | ||||
|  | ||||
| class Request: | ||||
| class Request(dict): | ||||
|     """ | ||||
|     Properties of an HTTP request such as URL, headers, etc. | ||||
|     """ | ||||
| @@ -61,21 +68,20 @@ class Request: | ||||
|             try: | ||||
|                 self.parsed_json = json_loads(self.body) | ||||
|             except Exception: | ||||
|                 pass | ||||
|                 raise InvalidUsage("Failed when parsing body as json") | ||||
|  | ||||
|         return self.parsed_json | ||||
|  | ||||
|     @property | ||||
|     def form(self): | ||||
|         if self.parsed_form is None: | ||||
|             self.parsed_form = {} | ||||
|             self.parsed_files = {} | ||||
|             content_type, parameters = parse_header( | ||||
|                 self.headers.get('Content-Type')) | ||||
|             self.parsed_form = RequestParameters() | ||||
|             self.parsed_files = RequestParameters() | ||||
|             content_type = self.headers.get( | ||||
|                 'Content-Type', DEFAULT_HTTP_CONTENT_TYPE) | ||||
|             content_type, parameters = parse_header(content_type) | ||||
|             try: | ||||
|                 is_url_encoded = ( | ||||
|                     content_type == 'application/x-www-form-urlencoded') | ||||
|                 if content_type is None or is_url_encoded: | ||||
|                 if content_type == 'application/x-www-form-urlencoded': | ||||
|                     self.parsed_form = RequestParameters( | ||||
|                         parse_qs(self.body.decode('utf-8'))) | ||||
|                 elif content_type == 'multipart/form-data': | ||||
| @@ -83,9 +89,8 @@ class Request: | ||||
|                     boundary = parameters['boundary'].encode('utf-8') | ||||
|                     self.parsed_form, self.parsed_files = ( | ||||
|                         parse_multipart_form(self.body, boundary)) | ||||
|             except Exception as e: | ||||
|                 log.exception(e) | ||||
|                 pass | ||||
|             except Exception: | ||||
|                 log.exception("Failed when parsing form") | ||||
|  | ||||
|         return self.parsed_form | ||||
|  | ||||
| @@ -110,9 +115,10 @@ class Request: | ||||
|     @property | ||||
|     def cookies(self): | ||||
|         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.load(self.headers['Cookie']) | ||||
|                 cookies.load(cookie) | ||||
|                 self._cookies = {name: cookie.value | ||||
|                                  for name, cookie in cookies.items()} | ||||
|             else: | ||||
| @@ -128,10 +134,10 @@ def parse_multipart_form(body, boundary): | ||||
|     Parses a request body and returns fields and files | ||||
|     :param body: Bytes request body | ||||
|     :param boundary: Bytes multipart boundary | ||||
|     :return: fields (dict), files (dict) | ||||
|     :return: fields (RequestParameters), files (RequestParameters) | ||||
|     """ | ||||
|     files = {} | ||||
|     fields = {} | ||||
|     files = RequestParameters() | ||||
|     fields = RequestParameters() | ||||
|  | ||||
|     form_parts = body.split(boundary) | ||||
|     for form_part in form_parts[1:-1]: | ||||
| @@ -162,9 +168,16 @@ def parse_multipart_form(body, boundary): | ||||
|  | ||||
|         post_data = form_part[line_index:-4] | ||||
|         if file_name or file_type: | ||||
|             files[field_name] = File( | ||||
|                 type=file_type, name=file_name, body=post_data) | ||||
|             file = File(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: | ||||
|             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 | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| from aiofiles import open as open_async | ||||
| from .cookies import CookieJar | ||||
| from mimetypes import guess_type | ||||
| from os import path | ||||
|  | ||||
| from ujson import dumps as json_dumps | ||||
|  | ||||
| from .cookies import CookieJar | ||||
|  | ||||
| COMMON_STATUS_CODES = { | ||||
|     200: b'OK', | ||||
|     400: b'Bad Request', | ||||
| @@ -79,7 +81,12 @@ class HTTPResponse: | ||||
|         self.content_type = content_type | ||||
|  | ||||
|         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: | ||||
|             self.body = body_bytes | ||||
|  | ||||
|   | ||||
| @@ -30,11 +30,17 @@ class Router: | ||||
|         @sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...]) | ||||
|         def my_route(request, my_parameter): | ||||
|             do stuff... | ||||
|     or | ||||
|         @sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...]) | ||||
|         def my_route_with_type(request, my_parameter): | ||||
|             do stuff... | ||||
|  | ||||
|     Parameters will be passed as keyword arguments to the request handling | ||||
|     function provided Parameters can also have a type by appending :type to | ||||
|     the <parameter>.  If no type is provided, a string is expected.  A regular | ||||
|     expression can also be passed in as the type | ||||
|     function. Provided parameters can also have a type by appending :type to | ||||
|     the <parameter>. Given parameter must be able to be type-casted to this. | ||||
|     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_dynamic = None | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| from asyncio import get_event_loop | ||||
| from collections import deque | ||||
| from functools import partial | ||||
| from inspect import isawaitable | ||||
| from inspect import isawaitable, stack, getmodulename | ||||
| from multiprocessing import Process, Event | ||||
| from signal import signal, SIGTERM, SIGINT | ||||
| from time import sleep | ||||
| @@ -18,7 +18,10 @@ from .exceptions import ServerError | ||||
|  | ||||
|  | ||||
| class Sanic: | ||||
|     def __init__(self, name, router=None, error_handler=None): | ||||
|     def __init__(self, name=None, router=None, error_handler=None): | ||||
|         if name is None: | ||||
|             frame_records = stack()[1] | ||||
|             name = getmodulename(frame_records[1]) | ||||
|         self.name = name | ||||
|         self.router = router or Router() | ||||
|         self.error_handler = error_handler or Handler(self) | ||||
| @@ -57,6 +60,19 @@ class Sanic: | ||||
|  | ||||
|         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 | ||||
|  | ||||
|     # Decorator | ||||
|     def exception(self, *exceptions): | ||||
|         """ | ||||
| @@ -95,13 +111,13 @@ class Sanic: | ||||
|             return register_middleware | ||||
|  | ||||
|     # Static Files | ||||
|     def static(self, file_or_directory, uri, pattern='.+', | ||||
|     def static(self, uri, file_or_directory, pattern='.+', | ||||
|                use_modified_since=True): | ||||
|         """ | ||||
|         Registers a root to serve files from.  The input can either be a file | ||||
|         or a directory.  See | ||||
|         """ | ||||
|         static_register(self, file_or_directory, uri, pattern, | ||||
|         static_register(self, uri, file_or_directory, pattern, | ||||
|                         use_modified_since) | ||||
|  | ||||
|     def blueprint(self, blueprint, **options): | ||||
| @@ -177,18 +193,18 @@ class Sanic: | ||||
|                 if isawaitable(response): | ||||
|                     response = await response | ||||
|  | ||||
|                 # -------------------------------------------- # | ||||
|                 # Response Middleware | ||||
|                 # -------------------------------------------- # | ||||
|             # -------------------------------------------- # | ||||
|             # Response Middleware | ||||
|             # -------------------------------------------- # | ||||
|  | ||||
|                 if self.response_middleware: | ||||
|                     for middleware in self.response_middleware: | ||||
|                         _response = middleware(request, response) | ||||
|                         if isawaitable(_response): | ||||
|                             _response = await _response | ||||
|                         if _response: | ||||
|                             response = _response | ||||
|                             break | ||||
|             if self.response_middleware: | ||||
|                 for middleware in self.response_middleware: | ||||
|                     _response = middleware(request, response) | ||||
|                     if isawaitable(_response): | ||||
|                         _response = await _response | ||||
|                     if _response: | ||||
|                         response = _response | ||||
|                         break | ||||
|  | ||||
|         except Exception as e: | ||||
|             # -------------------------------------------- # | ||||
| @@ -247,6 +263,7 @@ class Sanic: | ||||
|             'sock': sock, | ||||
|             'debug': debug, | ||||
|             'request_handler': self.handle_request, | ||||
|             'error_handler': self.error_handler, | ||||
|             'request_timeout': self.config.REQUEST_TIMEOUT, | ||||
|             'request_max_size': self.config.REQUEST_MAX_SIZE, | ||||
|             'loop': loop | ||||
| @@ -292,8 +309,7 @@ class Sanic: | ||||
|  | ||||
|         except Exception as e: | ||||
|             log.exception( | ||||
|                 'Experienced exception while trying to serve: {}'.format(e)) | ||||
|             pass | ||||
|                 'Experienced exception while trying to serve') | ||||
|  | ||||
|         log.info("Server Stopped") | ||||
|  | ||||
|   | ||||
							
								
								
									
										125
									
								
								sanic/server.py
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								sanic/server.py
									
									
									
									
									
								
							| @@ -1,8 +1,12 @@ | ||||
| import asyncio | ||||
| from functools import partial | ||||
| from inspect import isawaitable | ||||
| from multidict import CIMultiDict | ||||
| from signal import SIGINT, SIGTERM | ||||
|  | ||||
| import httptools | ||||
| from time import time | ||||
| from httptools import HttpRequestParser | ||||
| from httptools.parser.errors import HttpParserError | ||||
| from .exceptions import ServerError | ||||
|  | ||||
| try: | ||||
|     import uvloop as async_loop | ||||
| @@ -11,12 +15,16 @@ except ImportError: | ||||
|  | ||||
| from .log import log | ||||
| from .request import Request | ||||
| from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage | ||||
|  | ||||
|  | ||||
| class Signal: | ||||
|     stopped = False | ||||
|  | ||||
|  | ||||
| current_time = None | ||||
|  | ||||
|  | ||||
| class HttpProtocol(asyncio.Protocol): | ||||
|     __slots__ = ( | ||||
|         # event loop, connection | ||||
| @@ -26,10 +34,10 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         # request config | ||||
|         'request_handler', 'request_timeout', 'request_max_size', | ||||
|         # connection management | ||||
|         '_total_request_size', '_timeout_handler') | ||||
|         '_total_request_size', '_timeout_handler', '_last_communication_time') | ||||
|  | ||||
|     def __init__(self, *, loop, request_handler, signal=Signal(), | ||||
|                  connections={}, request_timeout=60, | ||||
|     def __init__(self, *, loop, request_handler, error_handler, | ||||
|                  signal=Signal(), connections={}, request_timeout=60, | ||||
|                  request_max_size=None): | ||||
|         self.loop = loop | ||||
|         self.transport = None | ||||
| @@ -40,32 +48,44 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         self.signal = signal | ||||
|         self.connections = connections | ||||
|         self.request_handler = request_handler | ||||
|         self.error_handler = error_handler | ||||
|         self.request_timeout = request_timeout | ||||
|         self.request_max_size = request_max_size | ||||
|         self._total_request_size = 0 | ||||
|         self._timeout_handler = None | ||||
|         self._last_request_time = None | ||||
|         self._request_handler_task = None | ||||
|  | ||||
|         # -------------------------------------------- # | ||||
|  | ||||
|     # -------------------------------------------- # | ||||
|     # Connection | ||||
|     # -------------------------------------------- # | ||||
|  | ||||
|     def connection_made(self, transport): | ||||
|         self.connections[self] = True | ||||
|         self.connections.add(self) | ||||
|         self._timeout_handler = self.loop.call_later( | ||||
|             self.request_timeout, self.connection_timeout) | ||||
|         self.transport = transport | ||||
|         self._last_request_time = current_time | ||||
|  | ||||
|     def connection_lost(self, exc): | ||||
|         del self.connections[self] | ||||
|         self.connections.discard(self) | ||||
|         self._timeout_handler.cancel() | ||||
|         self.cleanup() | ||||
|  | ||||
|     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 | ||||
|     # -------------------------------------------- # | ||||
|  | ||||
| @@ -74,37 +94,40 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         # memory limits | ||||
|         self._total_request_size += len(data) | ||||
|         if self._total_request_size > self.request_max_size: | ||||
|             return self.bail_out( | ||||
|                 "Request too large ({}), connection closed".format( | ||||
|                     self._total_request_size)) | ||||
|             exception = PayloadTooLarge('Payload Too Large') | ||||
|             self.write_error(exception) | ||||
|  | ||||
|         # Create parser if this is the first time we're receiving data | ||||
|         if self.parser is None: | ||||
|             assert self.request is None | ||||
|             self.headers = [] | ||||
|             self.parser = httptools.HttpRequestParser(self) | ||||
|             self.parser = HttpRequestParser(self) | ||||
|  | ||||
|         # Parse request chunk or close connection | ||||
|         try: | ||||
|             self.parser.feed_data(data) | ||||
|         except httptools.parser.errors.HttpParserError as e: | ||||
|             self.bail_out( | ||||
|                 "Invalid request data, connection closed ({})".format(e)) | ||||
|         except HttpParserError: | ||||
|             exception = InvalidUsage('Bad Request') | ||||
|             self.write_error(exception) | ||||
|  | ||||
|     def on_url(self, url): | ||||
|         self.url = url | ||||
|  | ||||
|     def on_header(self, name, value): | ||||
|         if name == b'Content-Length' and int(value) > self.request_max_size: | ||||
|             return self.bail_out( | ||||
|                 "Request body too large ({}), connection closed".format(value)) | ||||
|             exception = PayloadTooLarge('Payload Too Large') | ||||
|             self.write_error(exception) | ||||
|  | ||||
|         self.headers.append((name.decode(), value.decode('utf-8'))) | ||||
|  | ||||
|     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( | ||||
|             url_bytes=self.url, | ||||
|             headers=dict(self.headers), | ||||
|             headers=CIMultiDict(self.headers), | ||||
|             version=self.parser.get_http_version(), | ||||
|             method=self.parser.get_method().decode() | ||||
|         ) | ||||
| @@ -116,7 +139,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             self.request.body = body | ||||
|  | ||||
|     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)) | ||||
|  | ||||
|     # -------------------------------------------- # | ||||
| @@ -133,20 +156,34 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             if not keep_alive: | ||||
|                 self.transport.close() | ||||
|             else: | ||||
|                 # Record that we received data | ||||
|                 self._last_request_time = current_time | ||||
|                 self.cleanup() | ||||
|         except Exception as e: | ||||
|             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): | ||||
|         exception = ServerError(message) | ||||
|         self.write_error(exception) | ||||
|         log.error(message) | ||||
|         self.transport.close() | ||||
|  | ||||
|     def cleanup(self): | ||||
|         self.parser = None | ||||
|         self.request = None | ||||
|         self.url = None | ||||
|         self.headers = None | ||||
|         self._request_handler_task = None | ||||
|         self._total_request_size = 0 | ||||
|  | ||||
|     def close_if_idle(self): | ||||
| @@ -160,6 +197,18 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         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): | ||||
|     """ | ||||
|     :param events: one or more sync or async functions to execute | ||||
| @@ -174,8 +223,8 @@ def trigger_events(events, loop): | ||||
|                 loop.run_until_complete(result) | ||||
|  | ||||
|  | ||||
| def serve(host, port, request_handler, before_start=None, after_start=None, | ||||
|           before_stop=None, after_stop=None, | ||||
| def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|           after_start=None, before_stop=None, after_stop=None, | ||||
|           debug=False, request_timeout=60, sock=None, | ||||
|           request_max_size=None, reuse_port=False, loop=None): | ||||
|     """ | ||||
| @@ -203,16 +252,30 @@ def serve(host, port, request_handler, before_start=None, after_start=None, | ||||
|  | ||||
|     trigger_events(before_start, loop) | ||||
|  | ||||
|     connections = {} | ||||
|     connections = set() | ||||
|     signal = Signal() | ||||
|     server_coroutine = loop.create_server(lambda: HttpProtocol( | ||||
|     server = partial( | ||||
|         HttpProtocol, | ||||
|         loop=loop, | ||||
|         connections=connections, | ||||
|         signal=signal, | ||||
|         request_handler=request_handler, | ||||
|         error_handler=error_handler, | ||||
|         request_timeout=request_timeout, | ||||
|         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 | ||||
|     ) | ||||
|  | ||||
|     # Instead of pulling time at the end of every request, | ||||
|     # pull it once per minute | ||||
|     loop.call_soon(partial(update_current_time, loop)) | ||||
|  | ||||
|     try: | ||||
|         http_server = loop.run_until_complete(server_coroutine) | ||||
| @@ -240,7 +303,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, | ||||
|  | ||||
|         # Complete all tasks on the loop | ||||
|         signal.stopped = True | ||||
|         for connection in connections.keys(): | ||||
|         for connection in connections: | ||||
|             connection.close_if_idle() | ||||
|  | ||||
|         while connections: | ||||
|   | ||||
| @@ -2,12 +2,13 @@ from aiofiles.os import stat | ||||
| from os import path | ||||
| from re import sub | ||||
| from time import strftime, gmtime | ||||
| from urllib.parse import unquote | ||||
|  | ||||
| from .exceptions import FileNotFound, InvalidUsage | ||||
| from .response import file, HTTPResponse | ||||
|  | ||||
|  | ||||
| def register(app, file_or_directory, uri, pattern, use_modified_since): | ||||
| def register(app, uri, file_or_directory, pattern, use_modified_since): | ||||
|     # TODO: Though sanic is not a file server, I feel like we should atleast | ||||
|     #       make a good effort here.  Modified-since is nice, but we could | ||||
|     #       also look into etags, expires, and caching | ||||
| @@ -32,12 +33,17 @@ def register(app, file_or_directory, uri, pattern, use_modified_since): | ||||
|         # served.  os.path.realpath seems to be very slow | ||||
|         if file_uri and '../' in file_uri: | ||||
|             raise InvalidUsage("Invalid URL") | ||||
|  | ||||
|         # Merge served directory and requested file if provided | ||||
|         # 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 | ||||
|         file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ | ||||
|             if file_uri else file_or_directory | ||||
|         file_path = 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: | ||||
|             headers = {} | ||||
|             # Check if the client has been sent this file before | ||||
|   | ||||
| @@ -16,7 +16,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): | ||||
|  | ||||
|  | ||||
| def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||
|                         *request_args, **request_kwargs): | ||||
|                         loop=None, debug=False, *request_args, | ||||
|                         **request_kwargs): | ||||
|     results = [] | ||||
|     exceptions = [] | ||||
|  | ||||
| @@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||
|             exceptions.append(e) | ||||
|         app.stop() | ||||
|  | ||||
|     app.run(host=HOST, port=42101, after_start=_collect_response) | ||||
|     app.run(host=HOST, debug=debug, port=42101, | ||||
|             after_start=_collect_response, loop=loop) | ||||
|  | ||||
|     if 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 | ||||
|         except: | ||||
|             raise ValueError( | ||||
|                 "request and response object expected, got ({})".format( | ||||
|                 "Request and response object expected, got ({})".format( | ||||
|                     results)) | ||||
|     else: | ||||
|         try: | ||||
|             return results[0] | ||||
|         except: | ||||
|             raise ValueError( | ||||
|                 "request object expected, got ({})".format(results)) | ||||
|                 "Request object expected, got ({})".format(results)) | ||||
|   | ||||
							
								
								
									
										39
									
								
								sanic/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								sanic/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| 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(View): | ||||
|  | ||||
|             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(View): | ||||
|  | ||||
|             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(), '/') | ||||
|         2) app.route('/')(DummyView()) | ||||
|     """ | ||||
|  | ||||
|     def __call__(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) | ||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| @@ -30,6 +30,7 @@ setup( | ||||
|         'httptools>=0.0.9', | ||||
|         'ujson>=1.35', | ||||
|         'aiofiles>=0.3.0', | ||||
|         'multidict>=2.0', | ||||
|     ], | ||||
|     classifiers=[ | ||||
|         'Development Status :: 2 - Pre-Alpha', | ||||
|   | ||||
| @@ -15,4 +15,4 @@ async def handle(request): | ||||
| app = web.Application(loop=loop) | ||||
| 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 | ||||
|  | ||||
| import ( | ||||
|     "fmt" | ||||
|     "os" | ||||
|     "net/http" | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| type TestJSONResponse struct { | ||||
| 	Test bool | ||||
| } | ||||
|  | ||||
| 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() { | ||||
|     http.HandleFunc("/", handler) | ||||
|     http.ListenAndServe(":" + os.Args[1], nil) | ||||
| 	http.HandleFunc("/", handler) | ||||
| 	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' | ||||
| @@ -156,7 +156,7 @@ def test_bp_static(): | ||||
|     app = Sanic('test_static') | ||||
|     blueprint = Blueprint('test_static') | ||||
|  | ||||
|     blueprint.static(current_file, '/testing.file') | ||||
|     blueprint.static('/testing.file', current_file) | ||||
|  | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,19 @@ def test_cookies(): | ||||
|     assert response.text == 'Cookies are: working!' | ||||
|     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(): | ||||
|     app = Sanic('test_text') | ||||
|  | ||||
|   | ||||
							
								
								
									
										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.response import json, text | ||||
| from sanic.utils import sanic_endpoint_test | ||||
| from sanic.exceptions import ServerError | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| @@ -32,6 +33,22 @@ def test_text(): | ||||
|     assert response.text == 'Hello' | ||||
|  | ||||
|  | ||||
| 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(): | ||||
|     app = Sanic('test_json') | ||||
|  | ||||
| @@ -49,6 +66,19 @@ def test_json(): | ||||
|     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(): | ||||
|     app = Sanic('test_query_string') | ||||
|  | ||||
| @@ -56,7 +86,7 @@ def test_query_string(): | ||||
|     async def handler(request): | ||||
|         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('test2') == 'false' | ||||
|   | ||||
							
								
								
									
										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) | ||||
| @@ -84,7 +84,7 @@ def test_dynamic_route_int(): | ||||
|  | ||||
|  | ||||
| def test_dynamic_route_number(): | ||||
|     app = Sanic('test_dynamic_route_int') | ||||
|     app = Sanic('test_dynamic_route_number') | ||||
|  | ||||
|     results = [] | ||||
|  | ||||
| @@ -105,7 +105,7 @@ def test_dynamic_route_number(): | ||||
|  | ||||
|  | ||||
| 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}>') | ||||
|     async def handler(request, folder_id): | ||||
| @@ -145,7 +145,7 @@ def test_dynamic_route_unhashable(): | ||||
|  | ||||
|  | ||||
| def test_route_duplicate(): | ||||
|     app = Sanic('test_dynamic_route') | ||||
|     app = Sanic('test_route_duplicate') | ||||
|  | ||||
|     with pytest.raises(RouteExists): | ||||
|         @app.route('/test') | ||||
| @@ -178,3 +178,181 @@ def test_method_not_allowed(): | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, method='post', uri='/test') | ||||
|     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 | ||||
|   | ||||
| @@ -1,30 +1,62 @@ | ||||
| import inspect | ||||
| import os | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| 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.static(current_file, '/testing.file') | ||||
|     app.static('/testing.file', static_file_path) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/testing.file') | ||||
|     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()) | ||||
|     current_directory = os.path.dirname(os.path.abspath(current_file)) | ||||
|     with open(current_file, 'rb') as file: | ||||
|         current_file_contents = file.read() | ||||
|  | ||||
| def test_static_directory( | ||||
|         static_file_directory, static_file_path, static_file_content): | ||||
|  | ||||
|     app = Sanic('test_static') | ||||
|     app.static(current_directory, '/dir') | ||||
|     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.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 | ||||
|   | ||||
							
								
								
									
										155
									
								
								tests/test_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								tests/test_views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| 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(), '/') | ||||
|  | ||||
|     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(), '/') | ||||
|     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(), '/<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(), '/') | ||||
|  | ||||
|     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(), '/') | ||||
|  | ||||
|     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(), '/') | ||||
|  | ||||
|     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(), '/') | ||||
|  | ||||
|     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) | ||||
							
								
								
									
										18
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,27 +1,30 @@ | ||||
| [tox] | ||||
|  | ||||
| envlist = py35, report | ||||
| envlist = py35, py36 | ||||
|  | ||||
| [testenv] | ||||
|  | ||||
| deps = | ||||
|     aiohttp | ||||
|     pytest | ||||
|     # pytest-cov | ||||
|     coverage | ||||
|  | ||||
| commands = | ||||
|     coverage run -m pytest tests {posargs} | ||||
|     coverage run -m pytest -v tests {posargs} | ||||
|     mv .coverage .coverage.{envname} | ||||
|  | ||||
| basepython: | ||||
|     py35: python3.5 | ||||
|  | ||||
| whitelist_externals = | ||||
|     coverage | ||||
|     mv | ||||
|     echo | ||||
|  | ||||
| [testenv:flake8] | ||||
| deps = | ||||
|     flake8 | ||||
|  | ||||
| commands = | ||||
|     flake8 sanic | ||||
|  | ||||
| [testenv:report] | ||||
|  | ||||
| commands = | ||||
| @@ -29,6 +32,3 @@ commands = | ||||
|     coverage report | ||||
|     coverage html | ||||
|     echo "Open file://{toxinidir}/coverage/index.html" | ||||
|  | ||||
| basepython = | ||||
|     python3.5 | ||||
		Reference in New Issue
	
	Block a user