Compare commits
	
		
			157 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 98b08676e2 | ||
|   | 39f3a63ced | ||
|   | 89e2084489 | ||
|   | cce47a633a | ||
|   | ec2330c42b | ||
|   | ee89b6ad03 | ||
|   | a5e6d6d2e8 | ||
|   | 1eea1f5485 | ||
|   | da4567eea5 | ||
|   | 9010a6573f | ||
|   | d8e480ab48 | ||
|   | 0bd61f6a57 | ||
|   | c01cbb3a8c | ||
|   | 0ca5c4eeff | ||
|   | 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 | ||
|   | 977081f4af | ||
|   | ee70f1e55e | ||
|   | 9c16f6dbea | ||
|   | c50aa34dd9 | ||
|   | 0e479d53da | ||
|   | 984c086296 | ||
|   | 53e00b2b4c | ||
|   | bb1cb29edd | ||
|   | bf6879e46f | ||
|   | 12e900e8f9 | ||
|   | d7fff12b71 | ||
|   | 9051e985a0 | ||
|   | 5361c6f243 | ||
|   | 963aef19e0 | ||
|   | 201e232a0d | ||
|   | 6a71ea50bd | ||
|   | 47ec026536 | ||
|   | e70263d012 | ||
|   | 658ced9188 | ||
|   | 23290b8627 | ||
|   | 41ea40fc35 | ||
|   | 3802141007 | ||
|   | 50ae2048cc | ||
|   | b21ab3db12 | ||
|   | c80abb8cad | ||
|   | a3bd1eaeab | ||
|   | be0739614d | ||
|   | b048f1bad3 | ||
|   | c3628407eb | ||
|   | 96c13fe23c | ||
|   | ac9770dd89 | ||
|   | 0e2c092ce3 | ||
|   | 22876b31b1 | ||
|   | 113047d450 | ||
|   | 268a87e3b4 | ||
|   | 452764a8eb | ||
|   | f540f1e7c4 | ||
|   | 9b561e83e3 | ||
|   | 77c69e3810 | ||
|   | a5614f6880 | ||
|   | b74d312c57 | ||
|   | 2312a176fe | ||
|   | e060dbfec8 | ||
|   | 8f6e5a1263 | ||
|   | c256825de6 | ||
|   | cab43503d0 | ||
|   | d4e2d94816 | ||
|   | f510550888 | ||
|   | fc4c192237 | ||
|   | f4b45deb7f | ||
|   | d1beabfc8f | ||
|   | baf1ce95b1 | ||
|   | e25e1c0e4b | ||
|   | 04a6cc9416 | ||
|   | 50e4dd167e | ||
|   | f2cc404d7f | ||
|   | f6a8dbf486 | ||
|   | 7dcdc6208d | ||
|   | f5569f1723 | ||
|   | 0327e6efba | ||
|   | 138b947b95 | ||
|   | 3d00ca09b9 | ||
|   | 69345272cd | ||
|   | b6a06afdc0 | ||
|   | 2903e7ee7c | ||
|   | d5e4355a1c | ||
|   | 6d2d9d3afc | ||
|   | 71a783e7e1 | ||
|   | a6fa496c30 | ||
|   | f34fa40ed2 | ||
|   | c58741fe7a | ||
|   | 7b0f524fb3 | ||
|   | 5e459cb69d | ||
|   | cbb1f99ccb | ||
|   | 3c05382e07 | ||
|   | 7c3faea0dd | ||
|   | 452438dc07 | ||
|   | f95fe4192b | 
							
								
								
									
										22
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| Version 0.1 | ||||
| ----------- | ||||
|  - 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 | ||||
|   - Faster Router | ||||
|   - Fix: Incomplete file reads on medium+ sized post requests | ||||
|   - Breaking: after_start and before_stop now pass sanic as their first argument | ||||
|  - 0.1.4  | ||||
|   - Multiprocessing | ||||
|  - 0.1.3 | ||||
|   - Blueprint support | ||||
|   - Faster Response processing | ||||
|  - 0.1.1 - 0.1.2  | ||||
|   - Struggling to update pypi via CI | ||||
|  - 0.1.0  | ||||
|   - Released to public | ||||
							
								
								
									
										7
									
								
								CHANGES
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								CHANGES
									
									
									
									
									
								
							| @@ -1,7 +0,0 @@ | ||||
| Version 0.1 | ||||
| ----------- | ||||
|  - 0.1.4 - Multiprocessing | ||||
|  - 0.1.3 - Blueprint support | ||||
|  - 0.1.1 - 0.1.2 - Struggling to update pypi via CI | ||||
|  | ||||
| Released to public. | ||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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/) | ||||
| @@ -16,13 +18,14 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process.  E | ||||
|  | ||||
| | Server  | Implementation      | Requests/sec | Avg Latency | | ||||
| | ------- | ------------------- | ------------:| -----------:| | ||||
| | Sanic   | Python 3.5 + uvloop |       30,601 |      3.23ms | | ||||
| | Sanic   | Python 3.5 + uvloop |       33,342 |      2.96ms | | ||||
| | Wheezy  | gunicorn + meinheld |       20,244 |      4.94ms | | ||||
| | Falcon  | gunicorn + meinheld |       18,972 |      5.27ms | | ||||
| | Bottle  | gunicorn + meinheld |       13,596 |      7.36ms | | ||||
| | Flask   | gunicorn + meinheld |        4,988 |     20.08ms | | ||||
| | Kyoukai | Python 3.5 + uvloop |        3,889 |     27.44ms | | ||||
| | Aiohttp | Python 3.5 + uvloop |        2,979 |     33.42ms | | ||||
| | Tornado | Python 3.5          |        2,138 |     46.66ms | | ||||
|  | ||||
| ## Hello World | ||||
|  | ||||
| @@ -30,11 +33,11 @@ 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"}) | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000) | ||||
| ``` | ||||
| @@ -49,6 +52,9 @@ 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) | ||||
|  * [Deploying](docs/deploying.md) | ||||
|  * [Contributing](docs/contributing.md) | ||||
|  * [License](LICENSE) | ||||
|   | ||||
| @@ -29,7 +29,7 @@ from sanic import Blueprint | ||||
| bp = Blueprint('my_blueprint') | ||||
|  | ||||
| @bp.route('/') | ||||
| async def bp_root(): | ||||
| async def bp_root(request): | ||||
|     return json({'my': 'blueprint'}) | ||||
|  | ||||
| ``` | ||||
| @@ -42,7 +42,7 @@ from sanic import Sanic | ||||
| from my_blueprint import bp | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app.register_blueprint(bp) | ||||
| app.blueprint(bp) | ||||
|  | ||||
| app.run(host='0.0.0.0', port=8000, debug=True) | ||||
| ``` | ||||
| @@ -79,4 +79,33 @@ Exceptions can also be applied exclusively to blueprints globally. | ||||
| @bp.exception(NotFound) | ||||
| def ignore_404s(request, exception): | ||||
| 	return text("Yep, I totally found the page: {}".format(request.url)) | ||||
|  | ||||
| ## Static files | ||||
| Static files can also be served globally, under the blueprint prefix. | ||||
|  | ||||
| ```python | ||||
| bp.static('/folder/to/serve', '/web/path') | ||||
| ``` | ||||
|  | ||||
| ## Start and Stop | ||||
| Blueprints and run functions during the start and stop process of the server. | ||||
| If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork | ||||
| Available events are: | ||||
|  | ||||
|  * before_server_start - Executed before the server begins to accept connections | ||||
|  * after_server_start - Executed after the server begins to accept connections | ||||
|  * before_server_stop - Executed before the server stops accepting connections | ||||
|  * after_server_stop - Executed after the server is stopped and all requests are complete | ||||
|  | ||||
| ```python | ||||
| bp = Blueprint('my_blueprint') | ||||
|  | ||||
| @bp.listen('before_server_start') | ||||
| async def setup_connection(): | ||||
|     global database | ||||
|     database = mysql.connect(host='127.0.0.1'...) | ||||
|      | ||||
| @bp.listen('after_server_stop') | ||||
| async def close_connection(): | ||||
|     await database.close() | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										44
									
								
								docs/class_based_views.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								docs/class_based_views.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| # 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 | ||||
|  | ||||
| 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') | ||||
|  | ||||
| ``` | ||||
							
								
								
									
										50
									
								
								docs/cookies.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								docs/cookies.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| # Cookies | ||||
|  | ||||
| ## Request | ||||
|  | ||||
| Request cookies can be accessed via the request.cookie dictionary | ||||
|  | ||||
| ### Example | ||||
|  | ||||
| ```python | ||||
| from sanic import Sanic | ||||
| from sanic.response import text | ||||
|  | ||||
| @app.route("/cookie") | ||||
| async def test(request): | ||||
|     test_cookie = request.cookies.get('test') | ||||
|     return text("Test cookie set to: {}".format(test_cookie)) | ||||
| ``` | ||||
|  | ||||
| ## Response | ||||
|  | ||||
| Response cookies can be set like dictionary values and  | ||||
| have the following parameters available: | ||||
|  | ||||
| * expires - datetime - Time for cookie to expire on the client's browser | ||||
| * path - string - The Path attribute specifies the subset of URLs to  | ||||
|          which this cookie applies | ||||
| * comment - string - Cookie comment (metadata) | ||||
| * domain - string - Specifies the domain for which the | ||||
|            cookie is valid.  An explicitly specified domain must always  | ||||
|            start with a dot. | ||||
| * max-age - number - Number of seconds the cookie should live for | ||||
| * secure - boolean - Specifies whether the cookie will only be sent via | ||||
|            HTTPS | ||||
| * httponly - boolean - Specifies whether the cookie cannot be read | ||||
|              by javascript | ||||
|  | ||||
| ### Example | ||||
|  | ||||
| ```python | ||||
| from sanic import Sanic | ||||
| from sanic.response import text | ||||
|  | ||||
| @app.route("/cookie") | ||||
| async def test(request): | ||||
|     response = text("There's a cookie up in this response") | ||||
|     response.cookies['test'] = 'It worked!' | ||||
|     response.cookies['test']['domain'] = '.gotta-go-fast.com' | ||||
|     response.cookies['test']['httponly'] = True | ||||
|     return response | ||||
| ``` | ||||
| @@ -8,6 +8,7 @@ The following request variables are accessible as properties: | ||||
| `request.json` (any) - JSON body   | ||||
| `request.args` (dict) - Query String variables.  Use getlist to get multiple of the same name   | ||||
| `request.form` (dict) - Posted form variables.  Use getlist to get multiple of the same name   | ||||
| `request.body` (bytes) - Posted raw body.  To get the raw data, regardless of content type   | ||||
|  | ||||
| See request.py for more information | ||||
|  | ||||
| @@ -15,7 +16,7 @@ See request.py for more information | ||||
|  | ||||
| ```python | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| from sanic.response import json, text | ||||
|  | ||||
| @app.route("/json") | ||||
| def post_json(request): | ||||
| @@ -40,4 +41,9 @@ def post_json(request): | ||||
| @app.route("/query_string") | ||||
| def query_string(request): | ||||
|     return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) | ||||
|  | ||||
|  | ||||
| @app.route("/users", methods=["POST",]) | ||||
| def create_user(request): | ||||
|     return text("You are trying to create a user with the following POST: %s" % request.body) | ||||
| ``` | ||||
|   | ||||
| @@ -10,16 +10,16 @@ from sanic import Sanic | ||||
| from sanic.response import text | ||||
|  | ||||
| @app.route('/tag/<tag>') | ||||
| async def person_handler(request, tag): | ||||
| async def tag_handler(request, tag): | ||||
| 	return text('Tag - {}'.format(tag)) | ||||
|  | ||||
| @app.route('/number/<integer_arg:int>') | ||||
| async def person_handler(request, integer_arg): | ||||
| async def integer_handler(request, integer_arg): | ||||
| 	return text('Integer - {}'.format(integer_arg)) | ||||
|  | ||||
| @app.route('/number/<number_arg:number>') | ||||
| async def person_handler(request, number_arg): | ||||
| 	return text('Number - {}'.format(number)) | ||||
| async def number_handler(request, number_arg): | ||||
| 	return text('Number - {}'.format(number_arg)) | ||||
|  | ||||
| @app.route('/person/<name:[A-z]>') | ||||
| async def person_handler(request, name): | ||||
| @@ -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]>') | ||||
|  | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										18
									
								
								docs/static_files.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								docs/static_files.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Static Files | ||||
|  | ||||
| Both directories and files can be served by registering with static | ||||
|  | ||||
| ## Example | ||||
|  | ||||
| ```python | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| # Serves files from the static folder to the URL /static | ||||
| app.static('/static', './static') | ||||
|  | ||||
| # Serves the file /home/ubuntu/test.png when the URL /the_best.png | ||||
| # is requested | ||||
| app.static('/the_best.png', '/home/ubuntu/test.png') | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000) | ||||
| ``` | ||||
							
								
								
									
										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) | ||||
							
								
								
									
										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) | ||||
							
								
								
									
										80
									
								
								examples/sanic_peewee.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								examples/sanic_peewee.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| ## You need the following additional packages for this example | ||||
| # aiopg | ||||
| # peewee_async | ||||
| # peewee | ||||
|  | ||||
|  | ||||
| ## sanic imports | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
|  | ||||
| ## peewee_async related imports | ||||
| import uvloop | ||||
| import peewee | ||||
| from peewee_async import Manager, PostgresqlDatabase | ||||
|  | ||||
|  # we instantiate a custom loop so we can pass it to our db manager | ||||
| loop = uvloop.new_event_loop() | ||||
|  | ||||
| database = PostgresqlDatabase(database='test', | ||||
|                               host='127.0.0.1', | ||||
|                               user='postgres', | ||||
|                               password='mysecretpassword') | ||||
|  | ||||
| objects = Manager(database, loop=loop) | ||||
|  | ||||
| ## from peewee_async docs: | ||||
| # Also there’s no need to connect and re-connect before executing async queries | ||||
| # with manager! It’s all automatic. But you can run Manager.connect() or | ||||
| # Manager.close() when you need it. | ||||
|  | ||||
|  | ||||
| # let's create a simple key value store: | ||||
| class KeyValue(peewee.Model): | ||||
|     key = peewee.CharField(max_length=40, unique=True) | ||||
|     text = peewee.TextField(default='') | ||||
|  | ||||
|     class Meta: | ||||
|         database = database | ||||
|  | ||||
| # create table synchronously | ||||
| KeyValue.create_table(True) | ||||
|  | ||||
| # OPTIONAL: close synchronous connection | ||||
| database.close() | ||||
|  | ||||
| # OPTIONAL: disable any future syncronous calls | ||||
| objects.database.allow_sync = False # this will raise AssertionError on ANY sync call | ||||
|  | ||||
|  | ||||
| app = Sanic('peewee_example') | ||||
|  | ||||
| @app.route('/post/<key>/<value>') | ||||
| async def post(request, key, value): | ||||
|     """ | ||||
|     Save get parameters to database | ||||
|     """ | ||||
|     obj = await objects.create(KeyValue, key=key, text=value) | ||||
|     return json({'object_id': obj.id}) | ||||
|  | ||||
|  | ||||
| @app.route('/get') | ||||
| async def get(request): | ||||
|     """ | ||||
|     Load all objects from database | ||||
|     """ | ||||
|     all_objects = await objects.execute(KeyValue.select()) | ||||
|     serialized_obj = [] | ||||
|     for obj in all_objects: | ||||
|         serialized_obj.append({ | ||||
|             'id': obj.id, | ||||
|             'key': obj.key, | ||||
|             'value': obj.text} | ||||
|         ) | ||||
|  | ||||
|     return json({'objects': serialized_obj}) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     app.run(host='0.0.0.0', port=8000, loop=loop) | ||||
|  | ||||
| @@ -2,6 +2,7 @@ httptools | ||||
| ujson | ||||
| uvloop | ||||
| aiohttp | ||||
| aiocache | ||||
| pytest | ||||
| coverage | ||||
| tox | ||||
| @@ -9,3 +10,5 @@ gunicorn | ||||
| bottle | ||||
| kyoukai | ||||
| falcon | ||||
| tornado | ||||
| aiofiles | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| httptools | ||||
| ujson | ||||
| uvloop | ||||
| aiofiles | ||||
| multidict | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| from .sanic import Sanic | ||||
| from .blueprints import Blueprint | ||||
|  | ||||
| __version__ = '0.1.8' | ||||
|  | ||||
| __all__ = ['Sanic', 'Blueprint'] | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| from collections import defaultdict | ||||
|  | ||||
|  | ||||
| class BlueprintSetup: | ||||
|     """ | ||||
|     """ | ||||
| @@ -22,7 +25,7 @@ class BlueprintSetup: | ||||
|         if self.url_prefix: | ||||
|             uri = self.url_prefix + uri | ||||
|  | ||||
|         self.app.router.add(uri, methods, handler) | ||||
|         self.app.route(uri=uri, methods=methods)(handler) | ||||
|  | ||||
|     def add_exception(self, handler, *args, **kwargs): | ||||
|         """ | ||||
| @@ -30,6 +33,15 @@ class BlueprintSetup: | ||||
|         """ | ||||
|         self.app.exception(*args, **kwargs)(handler) | ||||
|  | ||||
|     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(uri, file_or_directory, *args, **kwargs) | ||||
|  | ||||
|     def add_middleware(self, middleware, *args, **kwargs): | ||||
|         """ | ||||
|         Registers middleware to sanic | ||||
| @@ -42,9 +54,15 @@ class BlueprintSetup: | ||||
|  | ||||
| class Blueprint: | ||||
|     def __init__(self, name, url_prefix=None): | ||||
|         """ | ||||
|         Creates a new blueprint | ||||
|         :param name: Unique name of the blueprint | ||||
|         :param url_prefix: URL to be prefixed before all route URLs | ||||
|         """ | ||||
|         self.name = name | ||||
|         self.url_prefix = url_prefix | ||||
|         self.deferred_functions = [] | ||||
|         self.listeners = defaultdict(list) | ||||
|  | ||||
|     def record(self, func): | ||||
|         """ | ||||
| @@ -73,6 +91,20 @@ 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): | ||||
|         """ | ||||
|         """ | ||||
|         def decorator(listener): | ||||
|             self.listeners[event].append(listener) | ||||
|             return listener | ||||
|         return decorator | ||||
|  | ||||
|     def middleware(self, *args, **kwargs): | ||||
|         """ | ||||
|         """ | ||||
| @@ -83,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 | ||||
|  | ||||
| @@ -95,3 +128,9 @@ class Blueprint: | ||||
|             self.record(lambda s: s.add_exception(handler, *args, **kwargs)) | ||||
|             return handler | ||||
|         return decorator | ||||
|  | ||||
|     def static(self, uri, file_or_directory, *args, **kwargs): | ||||
|         """ | ||||
|         """ | ||||
|         self.record( | ||||
|             lambda s: s.add_static(uri, file_or_directory, *args, **kwargs)) | ||||
|   | ||||
| @@ -22,3 +22,4 @@ class Config: | ||||
| """ | ||||
|     REQUEST_MAX_SIZE = 100000000  # 100 megababies | ||||
|     REQUEST_TIMEOUT = 60  # 60 seconds | ||||
|     ROUTER_CACHE_SIZE = 1024 | ||||
|   | ||||
							
								
								
									
										130
									
								
								sanic/cookies.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								sanic/cookies.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| from datetime import datetime | ||||
| import re | ||||
| import string | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  SimpleCookie | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| # Straight up copied this section of dark magic from SimpleCookie | ||||
|  | ||||
| _LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" | ||||
| _UnescapedChars = _LegalChars + ' ()/<=>?@[]{}' | ||||
|  | ||||
| _Translator = {n: '\\%03o' % n | ||||
|                for n in set(range(256)) - set(map(ord, _UnescapedChars))} | ||||
| _Translator.update({ | ||||
|     ord('"'): '\\"', | ||||
|     ord('\\'): '\\\\', | ||||
| }) | ||||
|  | ||||
|  | ||||
| def _quote(str): | ||||
|     r"""Quote a string for use in a cookie header. | ||||
|     If the string does not need to be double-quoted, then just return the | ||||
|     string.  Otherwise, surround the string in doublequotes and quote | ||||
|     (with a \) special characters. | ||||
|     """ | ||||
|     if str is None or _is_legal_key(str): | ||||
|         return str | ||||
|     else: | ||||
|         return '"' + str.translate(_Translator) + '"' | ||||
|  | ||||
|  | ||||
| _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  Custom SimpleCookie | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
|  | ||||
| class CookieJar(dict): | ||||
|     """ | ||||
|     CookieJar dynamically writes headers as cookies are added and removed | ||||
|     It gets around the limitation of one header per name by using the | ||||
|     MultiHeader class to provide a unique key that encodes to Set-Cookie | ||||
|     """ | ||||
|     def __init__(self, headers): | ||||
|         super().__init__() | ||||
|         self.headers = headers | ||||
|         self.cookie_headers = {} | ||||
|  | ||||
|     def __setitem__(self, key, value): | ||||
|         # If this cookie doesn't exist, add it to the header keys | ||||
|         cookie_header = self.cookie_headers.get(key) | ||||
|         if not cookie_header: | ||||
|             cookie = Cookie(key, value) | ||||
|             cookie_header = MultiHeader("Set-Cookie") | ||||
|             self.cookie_headers[key] = cookie_header | ||||
|             self.headers[cookie_header] = cookie | ||||
|             return super().__setitem__(key, cookie) | ||||
|         else: | ||||
|             self[key].value = value | ||||
|  | ||||
|     def __delitem__(self, key): | ||||
|         del self.cookie_headers[key] | ||||
|         return super().__delitem__(key) | ||||
|  | ||||
|  | ||||
| class Cookie(dict): | ||||
|     """ | ||||
|     This is a stripped down version of Morsel from SimpleCookie #gottagofast | ||||
|     """ | ||||
|     _keys = { | ||||
|         "expires": "expires", | ||||
|         "path": "Path", | ||||
|         "comment": "Comment", | ||||
|         "domain": "Domain", | ||||
|         "max-age": "Max-Age", | ||||
|         "secure": "Secure", | ||||
|         "httponly": "HttpOnly", | ||||
|         "version": "Version", | ||||
|     } | ||||
|     _flags = {'secure', 'httponly'} | ||||
|  | ||||
|     def __init__(self, key, value): | ||||
|         if key in self._keys: | ||||
|             raise KeyError("Cookie name is a reserved word") | ||||
|         if not _is_legal_key(key): | ||||
|             raise KeyError("Cookie key contains illegal characters") | ||||
|         self.key = key | ||||
|         self.value = value | ||||
|         super().__init__() | ||||
|  | ||||
|     def __setitem__(self, key, value): | ||||
|         if key not in self._keys: | ||||
|             raise KeyError("Unknown cookie property") | ||||
|         return super().__setitem__(key, value) | ||||
|  | ||||
|     def encode(self, encoding): | ||||
|         output = ['%s=%s' % (self.key, _quote(self.value))] | ||||
|         for key, value in self.items(): | ||||
|             if key == 'max-age' and isinstance(value, int): | ||||
|                 output.append('%s=%d' % (self._keys[key], value)) | ||||
|             elif key == 'expires' and isinstance(value, datetime): | ||||
|                 output.append('%s=%s' % ( | ||||
|                     self._keys[key], | ||||
|                     value.strftime("%a, %d-%b-%Y %T GMT") | ||||
|                 )) | ||||
|             elif key in self._flags: | ||||
|                 output.append(self._keys[key]) | ||||
|             else: | ||||
|                 output.append('%s=%s' % (self._keys[key], value)) | ||||
|  | ||||
|         return "; ".join(output).encode(encoding) | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  Header Trickery | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
|  | ||||
| class MultiHeader: | ||||
|     """ | ||||
|     Allows us to set a header within response that has a unique key, | ||||
|     but may contain duplicate header names | ||||
|     """ | ||||
|     def __init__(self, name): | ||||
|         self.name = name | ||||
|  | ||||
|     def encode(self): | ||||
|         return self.name.encode() | ||||
| @@ -21,6 +21,19 @@ class ServerError(SanicException): | ||||
|     status_code = 500 | ||||
|  | ||||
|  | ||||
| class FileNotFound(NotFound): | ||||
|     status_code = 404 | ||||
|  | ||||
|     def __init__(self, message, path, relative_url): | ||||
|         super().__init__(message) | ||||
|         self.path = path | ||||
|         self.relative_url = relative_url | ||||
|  | ||||
|  | ||||
| class RequestTimeout(SanicException): | ||||
|     status_code = 408 | ||||
|  | ||||
|  | ||||
| class Handler: | ||||
|     handlers = None | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| from cgi import parse_header | ||||
| from collections import namedtuple | ||||
| from http.cookies import SimpleCookie | ||||
| from httptools import parse_url | ||||
| from urllib.parse import parse_qs | ||||
| from ujson import loads as json_loads | ||||
| @@ -7,6 +8,12 @@ from ujson import loads as json_loads | ||||
| 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 | ||||
| @@ -25,12 +32,12 @@ class RequestParameters(dict): | ||||
|         return self.super.get(name, default) | ||||
|  | ||||
|  | ||||
| class Request: | ||||
| class Request(dict): | ||||
|     """ | ||||
|     Properties of an HTTP request such as URL, headers, etc. | ||||
|     """ | ||||
|     __slots__ = ( | ||||
|         'url', 'headers', 'version', 'method', | ||||
|         'url', 'headers', 'version', 'method', '_cookies', | ||||
|         'query_string', 'body', | ||||
|         'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', | ||||
|     ) | ||||
| @@ -52,6 +59,7 @@ class Request: | ||||
|         self.parsed_form = None | ||||
|         self.parsed_files = None | ||||
|         self.parsed_args = None | ||||
|         self._cookies = None | ||||
|  | ||||
|     @property | ||||
|     def json(self): | ||||
| @@ -59,21 +67,20 @@ class Request: | ||||
|             try: | ||||
|                 self.parsed_json = json_loads(self.body) | ||||
|             except Exception: | ||||
|                 pass | ||||
|                 log.exception("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': | ||||
| @@ -81,9 +88,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 | ||||
|  | ||||
| @@ -105,6 +111,18 @@ class Request: | ||||
|  | ||||
|         return self.parsed_args | ||||
|  | ||||
|     @property | ||||
|     def cookies(self): | ||||
|         if self._cookies is None: | ||||
|             if 'Cookie' in self.headers: | ||||
|                 cookies = SimpleCookie() | ||||
|                 cookies.load(self.headers['Cookie']) | ||||
|                 self._cookies = {name: cookie.value | ||||
|                                  for name, cookie in cookies.items()} | ||||
|             else: | ||||
|                 self._cookies = {} | ||||
|         return self._cookies | ||||
|  | ||||
|  | ||||
| File = namedtuple('File', ['type', 'body', 'name']) | ||||
|  | ||||
| @@ -114,10 +132,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]: | ||||
| @@ -148,9 +166,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: | ||||
|             fields[field_name] = post_data.decode('utf-8') | ||||
|                 files[field_name] = [file] | ||||
|         else: | ||||
|             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,23 +1,78 @@ | ||||
| import ujson | ||||
| 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 | ||||
|  | ||||
| STATUS_CODES = { | ||||
| COMMON_STATUS_CODES = { | ||||
|     200: b'OK', | ||||
|     400: b'Bad Request', | ||||
|     404: b'Not Found', | ||||
|     500: b'Internal Server Error', | ||||
| } | ||||
| ALL_STATUS_CODES = { | ||||
|     100: b'Continue', | ||||
|     101: b'Switching Protocols', | ||||
|     102: b'Processing', | ||||
|     200: b'OK', | ||||
|     201: b'Created', | ||||
|     202: b'Accepted', | ||||
|     203: b'Non-Authoritative Information', | ||||
|     204: b'No Content', | ||||
|     205: b'Reset Content', | ||||
|     206: b'Partial Content', | ||||
|     207: b'Multi-Status', | ||||
|     208: b'Already Reported', | ||||
|     226: b'IM Used', | ||||
|     300: b'Multiple Choices', | ||||
|     301: b'Moved Permanently', | ||||
|     302: b'Found', | ||||
|     303: b'See Other', | ||||
|     304: b'Not Modified', | ||||
|     305: b'Use Proxy', | ||||
|     307: b'Temporary Redirect', | ||||
|     308: b'Permanent Redirect', | ||||
|     400: b'Bad Request', | ||||
|     401: b'Unauthorized', | ||||
|     402: b'Payment Required', | ||||
|     403: b'Forbidden', | ||||
|     404: b'Not Found', | ||||
|     405: b'Method Not Allowed', | ||||
|     406: b'Not Acceptable', | ||||
|     407: b'Proxy Authentication Required', | ||||
|     408: b'Request Timeout', | ||||
|     409: b'Conflict', | ||||
|     410: b'Gone', | ||||
|     411: b'Length Required', | ||||
|     412: b'Precondition Failed', | ||||
|     413: b'Request Entity Too Large', | ||||
|     414: b'Request-URI Too Long', | ||||
|     415: b'Unsupported Media Type', | ||||
|     416: b'Requested Range Not Satisfiable', | ||||
|     417: b'Expectation Failed', | ||||
|     422: b'Unprocessable Entity', | ||||
|     423: b'Locked', | ||||
|     424: b'Failed Dependency', | ||||
|     426: b'Upgrade Required', | ||||
|     428: b'Precondition Required', | ||||
|     429: b'Too Many Requests', | ||||
|     431: b'Request Header Fields Too Large', | ||||
|     500: b'Internal Server Error', | ||||
|     501: b'Not Implemented', | ||||
|     502: b'Bad Gateway', | ||||
|     503: b'Service Unavailable', | ||||
|     504: b'Gateway Timeout', | ||||
|     505: b'HTTP Version Not Supported', | ||||
|     506: b'Variant Also Negotiates', | ||||
|     507: b'Insufficient Storage', | ||||
|     508: b'Loop Detected', | ||||
|     510: b'Not Extended', | ||||
|     511: b'Network Authentication Required' | ||||
| } | ||||
|  | ||||
|  | ||||
| class HTTPResponse: | ||||
|     __slots__ = ('body', 'status', 'content_type', 'headers') | ||||
|     __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') | ||||
|  | ||||
|     def __init__(self, body=None, status=200, headers=None, | ||||
|                  content_type='text/plain', body_bytes=b''): | ||||
| @@ -30,6 +85,7 @@ class HTTPResponse: | ||||
|  | ||||
|         self.status = status | ||||
|         self.headers = headers or {} | ||||
|         self._cookies = None | ||||
|  | ||||
|     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||
|         # This is all returned in a kind-of funky way | ||||
| @@ -44,6 +100,13 @@ class HTTPResponse: | ||||
|                 b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) | ||||
|                 for name, value in self.headers.items() | ||||
|             ) | ||||
|  | ||||
|         # Try to pull from the common codes first | ||||
|         # Speeds up response rate 6% over pulling from all | ||||
|         status = COMMON_STATUS_CODES.get(self.status) | ||||
|         if not status: | ||||
|             status = ALL_STATUS_CODES.get(self.status) | ||||
|  | ||||
|         return (b'HTTP/%b %d %b\r\n' | ||||
|                 b'Content-Type: %b\r\n' | ||||
|                 b'Content-Length: %d\r\n' | ||||
| @@ -52,7 +115,7 @@ class HTTPResponse: | ||||
|                 b'%b') % ( | ||||
|             version.encode(), | ||||
|             self.status, | ||||
|             STATUS_CODES.get(self.status, b'FAIL'), | ||||
|             status, | ||||
|             self.content_type.encode(), | ||||
|             len(self.body), | ||||
|             b'keep-alive' if keep_alive else b'close', | ||||
| @@ -61,10 +124,16 @@ class HTTPResponse: | ||||
|             self.body | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def cookies(self): | ||||
|         if self._cookies is None: | ||||
|             self._cookies = CookieJar(self.headers) | ||||
|         return self._cookies | ||||
|  | ||||
|  | ||||
| def json(body, status=200, headers=None): | ||||
|     return HTTPResponse(ujson.dumps(body), headers=headers, status=status, | ||||
|                         content_type="application/json; charset=utf-8") | ||||
|     return HTTPResponse(json_dumps(body), headers=headers, status=status, | ||||
|                         content_type="application/json") | ||||
|  | ||||
|  | ||||
| def text(body, status=200, headers=None): | ||||
| @@ -75,3 +144,17 @@ def text(body, status=200, headers=None): | ||||
| def html(body, status=200, headers=None): | ||||
|     return HTTPResponse(body, status=status, headers=headers, | ||||
|                         content_type="text/html; charset=utf-8") | ||||
|  | ||||
|  | ||||
| async def file(location, mime_type=None, headers=None): | ||||
|     filename = path.split(location)[-1] | ||||
|  | ||||
|     async with open_async(location, mode='rb') as _file: | ||||
|         out_stream = await _file.read() | ||||
|  | ||||
|     mime_type = mime_type or guess_type(filename)[0] or 'text/plain' | ||||
|  | ||||
|     return HTTPResponse(status=200, | ||||
|                         headers=headers, | ||||
|                         content_type=mime_type, | ||||
|                         body_bytes=out_stream) | ||||
|   | ||||
							
								
								
									
										177
									
								
								sanic/router.py
									
									
									
									
									
								
							
							
						
						
									
										177
									
								
								sanic/router.py
									
									
									
									
									
								
							| @@ -1,9 +1,26 @@ | ||||
| import re | ||||
| from collections import namedtuple | ||||
| from collections import defaultdict, namedtuple | ||||
| from functools import lru_cache | ||||
| from .config import Config | ||||
| from .exceptions import NotFound, InvalidUsage | ||||
|  | ||||
| Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) | ||||
| Parameter = namedtuple("Parameter", ['name', 'cast']) | ||||
| Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) | ||||
| Parameter = namedtuple('Parameter', ['name', 'cast']) | ||||
|  | ||||
| REGEX_TYPES = { | ||||
|     'string': (str, r'[^/]+'), | ||||
|     'int': (int, r'\d+'), | ||||
|     'number': (float, r'[0-9\\.]+'), | ||||
|     'alpha': (str, r'[A-Za-z]+'), | ||||
| } | ||||
|  | ||||
|  | ||||
| def url_hash(url): | ||||
|     return url.count('/') | ||||
|  | ||||
|  | ||||
| class RouteExists(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class Router: | ||||
| @@ -18,22 +35,16 @@ class Router: | ||||
|     function provided Parameters can also have a type by appending :type to | ||||
|     the <parameter>.  If no type is provided, a string is expected.  A regular | ||||
|     expression can also be passed in as the type | ||||
|  | ||||
|     TODO: | ||||
|         This probably needs optimization for larger sets of routes, | ||||
|         since it checks every route until it finds a match which is bad and | ||||
|         I should feel bad | ||||
|     """ | ||||
|     routes = None | ||||
|     regex_types = { | ||||
|         "string": (None, "[^/]+"), | ||||
|         "int": (int, "\d+"), | ||||
|         "number": (float, "[0-9\\.]+"), | ||||
|         "alpha": (None, "[A-Za-z]+"), | ||||
|     } | ||||
|     routes_static = None | ||||
|     routes_dynamic = None | ||||
|     routes_always_check = None | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.routes = [] | ||||
|         self.routes_all = {} | ||||
|         self.routes_static = {} | ||||
|         self.routes_dynamic = defaultdict(list) | ||||
|         self.routes_always_check = [] | ||||
|  | ||||
|     def add(self, uri, methods, handler): | ||||
|         """ | ||||
| @@ -45,42 +56,52 @@ class Router: | ||||
|         When executed, it should provide a response object. | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         if uri in self.routes_all: | ||||
|             raise RouteExists("Route already registered: {}".format(uri)) | ||||
|  | ||||
|         # Dict for faster lookups of if method allowed | ||||
|         methods_dict = None | ||||
|         if methods: | ||||
|             methods_dict = {method: True for method in methods} | ||||
|             methods = frozenset(methods) | ||||
|  | ||||
|         parameters = [] | ||||
|         properties = {"unhashable": None} | ||||
|  | ||||
|         def add_parameter(match): | ||||
|             # We could receive NAME or NAME:PATTERN | ||||
|             parts = match.group(1).split(':') | ||||
|             if len(parts) == 2: | ||||
|                 parameter_name, parameter_pattern = parts | ||||
|             else: | ||||
|                 parameter_name = parts[0] | ||||
|                 parameter_pattern = 'string' | ||||
|             name = match.group(1) | ||||
|             pattern = 'string' | ||||
|             if ':' in name: | ||||
|                 name, pattern = name.split(':', 1) | ||||
|  | ||||
|             default = (str, pattern) | ||||
|             # Pull from pre-configured types | ||||
|             parameter_regex = self.regex_types.get(parameter_pattern) | ||||
|             if parameter_regex: | ||||
|                 parameter_type, parameter_pattern = parameter_regex | ||||
|             else: | ||||
|                 parameter_type = None | ||||
|  | ||||
|             parameter = Parameter(name=parameter_name, cast=parameter_type) | ||||
|             _type, pattern = REGEX_TYPES.get(pattern, default) | ||||
|             parameter = Parameter(name=name, cast=_type) | ||||
|             parameters.append(parameter) | ||||
|  | ||||
|             return "({})".format(parameter_pattern) | ||||
|             # Mark the whole route as unhashable if it has the hash key in it | ||||
|             if re.search('(^|[^^]){1}/', pattern): | ||||
|                 properties['unhashable'] = True | ||||
|             # Mark the route as unhashable if it matches the hash key | ||||
|             elif re.search(pattern, '/'): | ||||
|                 properties['unhashable'] = True | ||||
|  | ||||
|         pattern_string = re.sub("<(.+?)>", add_parameter, uri) | ||||
|         pattern = re.compile("^{}$".format(pattern_string)) | ||||
|             return '({})'.format(pattern) | ||||
|  | ||||
|         pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) | ||||
|         pattern = re.compile(r'^{}$'.format(pattern_string)) | ||||
|  | ||||
|         route = Route( | ||||
|             handler=handler, methods=methods_dict, pattern=pattern, | ||||
|             handler=handler, methods=methods, pattern=pattern, | ||||
|             parameters=parameters) | ||||
|         self.routes.append(route) | ||||
|  | ||||
|         self.routes_all[uri] = route | ||||
|         if properties['unhashable']: | ||||
|             self.routes_always_check.append(route) | ||||
|         elif parameters: | ||||
|             self.routes_dynamic[url_hash(uri)].append(route) | ||||
|         else: | ||||
|             self.routes_static[uri] = route | ||||
|  | ||||
|     def get(self, request): | ||||
|         """ | ||||
| @@ -89,58 +110,42 @@ class Router: | ||||
|         :param request: Request object | ||||
|         :return: handler, arguments, keyword arguments | ||||
|         """ | ||||
|         return self._get(request.url, request.method) | ||||
|  | ||||
|         route = None | ||||
|         args = [] | ||||
|         kwargs = {} | ||||
|         for _route in self.routes: | ||||
|             match = _route.pattern.match(request.url) | ||||
|     @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) | ||||
|     def _get(self, url, method): | ||||
|         """ | ||||
|         Gets a request handler based on the URL of the request, or raises an | ||||
|         error.  Internal method for caching. | ||||
|         :param url: Request URL | ||||
|         :param method: Request method | ||||
|         :return: handler, arguments, keyword arguments | ||||
|         """ | ||||
|         # Check against known static routes | ||||
|         route = self.routes_static.get(url) | ||||
|         if route: | ||||
|             match = route.pattern.match(url) | ||||
|         else: | ||||
|             # Move on to testing all regex routes | ||||
|             for route in self.routes_dynamic[url_hash(url)]: | ||||
|                 match = route.pattern.match(url) | ||||
|                 if match: | ||||
|                 for index, parameter in enumerate(_route.parameters, start=1): | ||||
|                     value = match.group(index) | ||||
|                     if parameter.cast: | ||||
|                         kwargs[parameter.name] = parameter.cast(value) | ||||
|                     else: | ||||
|                         kwargs[parameter.name] = value | ||||
|                 route = _route | ||||
|                     break | ||||
|  | ||||
|         if route: | ||||
|             if route.methods and request.method not in route.methods: | ||||
|                 raise InvalidUsage( | ||||
|                     "Method {} not allowed for URL {}".format( | ||||
|                         request.method, request.url), status_code=405) | ||||
|             return route.handler, args, kwargs | ||||
|             else: | ||||
|             raise NotFound("Requested URL {} not found".format(request.url)) | ||||
|  | ||||
|  | ||||
| class SimpleRouter: | ||||
|     """ | ||||
|     Simple router records and reads all routes from a dictionary | ||||
|     It does not support parameters in routes, but is very fast | ||||
|     """ | ||||
|     routes = None | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.routes = {} | ||||
|  | ||||
|     def add(self, uri, methods, handler): | ||||
|         # Dict for faster lookups of method allowed | ||||
|         methods_dict = None | ||||
|         if methods: | ||||
|             methods_dict = {method: True for method in methods} | ||||
|         self.routes[uri] = Route( | ||||
|             handler=handler, methods=methods_dict, pattern=uri, | ||||
|             parameters=None) | ||||
|  | ||||
|     def get(self, request): | ||||
|         route = self.routes.get(request.url) | ||||
|         if route: | ||||
|             if route.methods and request.method not in route.methods: | ||||
|                 raise InvalidUsage( | ||||
|                     "Method {} not allowed for URL {}".format( | ||||
|                         request.method, request.url), status_code=405) | ||||
|             return route.handler, [], {} | ||||
|                 # Lastly, check against all regex routes that cannot be hashed | ||||
|                 for route in self.routes_always_check: | ||||
|                     match = route.pattern.match(url) | ||||
|                     if match: | ||||
|                         break | ||||
|                 else: | ||||
|             raise NotFound("Requested URL {} not found".format(request.url)) | ||||
|                     raise NotFound('Requested URL {} not found'.format(url)) | ||||
|  | ||||
|         if route.methods and method not in route.methods: | ||||
|             raise InvalidUsage( | ||||
|                 'Method {} not allowed for URL {}'.format( | ||||
|                     method, url), status_code=405) | ||||
|  | ||||
|         kwargs = {p.name: p.cast(value) | ||||
|                   for value, p | ||||
|                   in zip(match.groups(1), route.parameters)} | ||||
|         return route.handler, [], kwargs | ||||
|   | ||||
							
								
								
									
										128
									
								
								sanic/sanic.py
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								sanic/sanic.py
									
									
									
									
									
								
							| @@ -1,5 +1,7 @@ | ||||
| from asyncio import get_event_loop | ||||
| from inspect import isawaitable | ||||
| from collections import deque | ||||
| from functools import partial | ||||
| from inspect import isawaitable, stack, getmodulename | ||||
| from multiprocessing import Process, Event | ||||
| from signal import signal, SIGTERM, SIGINT | ||||
| from time import sleep | ||||
| @@ -11,19 +13,28 @@ from .log import log, logging | ||||
| from .response import HTTPResponse | ||||
| from .router import Router | ||||
| from .server import serve | ||||
| from .static import register as static_register | ||||
| 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) | ||||
|         self.config = Config() | ||||
|         self.request_middleware = [] | ||||
|         self.response_middleware = [] | ||||
|         self.request_middleware = deque() | ||||
|         self.response_middleware = deque() | ||||
|         self.blueprints = {} | ||||
|         self._blueprint_order = [] | ||||
|         self.loop = None | ||||
|         self.debug = None | ||||
|  | ||||
|         # Register alternative method names | ||||
|         self.go_fast = self.run | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Registration | ||||
| @@ -38,18 +49,35 @@ class Sanic: | ||||
|         :return: decorated function | ||||
|         """ | ||||
|  | ||||
|         # Fix case where the user did not prefix the URL with a / | ||||
|         # and will probably get confused as to why it's not working | ||||
|         if not uri.startswith('/'): | ||||
|             uri = '/' + uri | ||||
|  | ||||
|         def response(handler): | ||||
|             self.router.add(uri=uri, methods=methods, handler=handler) | ||||
|             return handler | ||||
|  | ||||
|         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): | ||||
|         """ | ||||
|         Decorates a function to be registered as a route | ||||
|         :param uri: path of the URL | ||||
|         :param methods: list or tuple of methods allowed | ||||
|         Decorates a function to be registered as a handler for exceptions | ||||
|         :param *exceptions: exceptions | ||||
|         :return: decorated function | ||||
|         """ | ||||
|  | ||||
| @@ -72,7 +100,7 @@ class Sanic: | ||||
|             if attach_to == 'request': | ||||
|                 self.request_middleware.append(middleware) | ||||
|             if attach_to == 'response': | ||||
|                 self.response_middleware.append(middleware) | ||||
|                 self.response_middleware.appendleft(middleware) | ||||
|             return middleware | ||||
|  | ||||
|         # Detect which way this was called, @middleware or @middleware('AT') | ||||
| @@ -82,7 +110,17 @@ class Sanic: | ||||
|             attach_to = args[0] | ||||
|             return register_middleware | ||||
|  | ||||
|     def register_blueprint(self, blueprint, **options): | ||||
|     # Static Files | ||||
|     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, uri, file_or_directory, pattern, | ||||
|                         use_modified_since) | ||||
|  | ||||
|     def blueprint(self, blueprint, **options): | ||||
|         """ | ||||
|         Registers a blueprint on the application. | ||||
|         :param blueprint: Blueprint object | ||||
| @@ -99,10 +137,19 @@ class Sanic: | ||||
|             self._blueprint_order.append(blueprint) | ||||
|         blueprint.register(self, options) | ||||
|  | ||||
|     def register_blueprint(self, *args, **kwargs): | ||||
|         # TODO: deprecate 1.0 | ||||
|         log.warning("Use of register_blueprint will be deprecated in " | ||||
|                     "version 1.0.  Please use the blueprint method instead") | ||||
|         return self.blueprint(*args, **kwargs) | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Request Handling | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     def converted_response_type(self, response): | ||||
|         pass | ||||
|  | ||||
|     async def handle_request(self, request, response_callback): | ||||
|         """ | ||||
|         Takes a request from the HTTP Server and returns a response object to | ||||
| @@ -114,7 +161,10 @@ class Sanic: | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         try: | ||||
|             # Middleware process_request | ||||
|             # -------------------------------------------- # | ||||
|             # Request Middleware | ||||
|             # -------------------------------------------- # | ||||
|  | ||||
|             response = False | ||||
|             # The if improves speed.  I don't know why | ||||
|             if self.request_middleware: | ||||
| @@ -127,6 +177,10 @@ class Sanic: | ||||
|  | ||||
|             # No middleware results | ||||
|             if not response: | ||||
|                 # -------------------------------------------- # | ||||
|                 # Execute Handler | ||||
|                 # -------------------------------------------- # | ||||
|  | ||||
|                 # Fetch handler from router | ||||
|                 handler, args, kwargs = self.router.get(request) | ||||
|                 if handler is None: | ||||
| @@ -139,7 +193,10 @@ class Sanic: | ||||
|                 if isawaitable(response): | ||||
|                     response = await response | ||||
|  | ||||
|                 # Middleware process_response | ||||
|                 # -------------------------------------------- # | ||||
|                 # Response Middleware | ||||
|                 # -------------------------------------------- # | ||||
|  | ||||
|                 if self.response_middleware: | ||||
|                     for middleware in self.response_middleware: | ||||
|                         _response = middleware(request, response) | ||||
| @@ -150,6 +207,10 @@ class Sanic: | ||||
|                             break | ||||
|  | ||||
|         except Exception as e: | ||||
|             # -------------------------------------------- # | ||||
|             # Response Generation Failed | ||||
|             # -------------------------------------------- # | ||||
|  | ||||
|             try: | ||||
|                 response = self.error_handler.response(request, e) | ||||
|                 if isawaitable(response): | ||||
| @@ -169,25 +230,32 @@ class Sanic: | ||||
|     # Execution | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, | ||||
|             before_stop=None, sock=None, workers=1): | ||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, | ||||
|             after_start=None, before_stop=None, after_stop=None, sock=None, | ||||
|             workers=1, loop=None): | ||||
|         """ | ||||
|         Runs the HTTP Server and listens until keyboard interrupt or term | ||||
|         signal. On termination, drains connections before closing. | ||||
|         :param host: Address to host on | ||||
|         :param port: Port to host on | ||||
|         :param debug: Enables debug output (slows server) | ||||
|         :param before_start: Function to be executed before the server starts | ||||
|         accepting connections | ||||
|         :param after_start: Function to be executed after the server starts | ||||
|         listening | ||||
|         accepting connections | ||||
|         :param before_stop: Function to be executed when a stop signal is | ||||
|         received before it is respected | ||||
|         :param after_stop: Function to be executed when all requests are | ||||
|         complete | ||||
|         :param sock: Socket for the server to accept connections from | ||||
|         :param workers: Number of processes | ||||
|         received before it is respected | ||||
|         :param loop: asyncio compatible event loop | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         self.error_handler.debug = True | ||||
|         self.debug = debug | ||||
|         self.loop = loop | ||||
|  | ||||
|         server_settings = { | ||||
|             'host': host, | ||||
| @@ -195,10 +263,35 @@ 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 | ||||
|         } | ||||
|  | ||||
|         # -------------------------------------------- # | ||||
|         # Register start/stop events | ||||
|         # -------------------------------------------- # | ||||
|  | ||||
|         for event_name, settings_name, args, reverse in ( | ||||
|                 ("before_server_start", "before_start", before_start, False), | ||||
|                 ("after_server_start", "after_start", after_start, False), | ||||
|                 ("before_server_stop", "before_stop", before_stop, True), | ||||
|                 ("after_server_stop", "after_stop", after_stop, True), | ||||
|                 ): | ||||
|             listeners = [] | ||||
|             for blueprint in self.blueprints.values(): | ||||
|                 listeners += blueprint.listeners[event_name] | ||||
|             if args: | ||||
|                 if type(args) is not list: | ||||
|                     args = [args] | ||||
|                 listeners += args | ||||
|             if reverse: | ||||
|                 listeners.reverse() | ||||
|             # Prepend sanic to the arguments when listeners are triggered | ||||
|             listeners = [partial(listener, self) for listener in listeners] | ||||
|             server_settings[settings_name] = listeners | ||||
|  | ||||
|         if debug: | ||||
|             log.setLevel(logging.DEBUG) | ||||
|         log.debug(self.config.LOGO) | ||||
| @@ -208,8 +301,6 @@ class Sanic: | ||||
|  | ||||
|         try: | ||||
|             if workers == 1: | ||||
|                 server_settings['after_start'] = after_start | ||||
|                 server_settings['before_stop'] = before_stop | ||||
|                 serve(**server_settings) | ||||
|             else: | ||||
|                 log.info('Spinning up {} workers...'.format(workers)) | ||||
| @@ -218,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") | ||||
|  | ||||
| @@ -248,7 +338,7 @@ class Sanic: | ||||
|         signal(SIGTERM, lambda s, f: stop_event.set()) | ||||
|  | ||||
|         processes = [] | ||||
|         for w in range(workers): | ||||
|         for _ in range(workers): | ||||
|             process = Process(target=serve, kwargs=server_settings) | ||||
|             process.start() | ||||
|             processes.append(process) | ||||
|   | ||||
							
								
								
									
										140
									
								
								sanic/server.py
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								sanic/server.py
									
									
									
									
									
								
							| @@ -1,8 +1,11 @@ | ||||
| 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 | ||||
|  | ||||
| try: | ||||
|     import uvloop as async_loop | ||||
| @@ -11,12 +14,16 @@ except ImportError: | ||||
|  | ||||
| from .log import log | ||||
| from .request import Request | ||||
| from .exceptions import RequestTimeout | ||||
|  | ||||
|  | ||||
| class Signal: | ||||
|     stopped = False | ||||
|  | ||||
|  | ||||
| current_time = None | ||||
|  | ||||
|  | ||||
| class HttpProtocol(asyncio.Protocol): | ||||
|     __slots__ = ( | ||||
|         # event loop, connection | ||||
| @@ -26,10 +33,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,13 +47,15 @@ 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 | ||||
|     # -------------------------------------------- # | ||||
|  | ||||
| @@ -55,6 +64,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         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] | ||||
| @@ -62,10 +72,20 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         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() | ||||
|             response = self.error_handler.response( | ||||
|                 self.request, RequestTimeout('Request Timeout')) | ||||
|             self.write_response(response) | ||||
|  | ||||
|     # -------------------------------------------- # | ||||
|  | ||||
|     # Parsing | ||||
|     # -------------------------------------------- # | ||||
|  | ||||
| @@ -82,12 +102,12 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         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: | ||||
|         except HttpParserError as e: | ||||
|             self.bail_out( | ||||
|                 "Invalid request data, connection closed ({})".format(e)) | ||||
|  | ||||
| @@ -102,18 +122,25 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         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() | ||||
|         ) | ||||
|  | ||||
|     def on_body(self, body): | ||||
|         if self.request.body: | ||||
|             self.request.body += body | ||||
|         else: | ||||
|             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)) | ||||
|  | ||||
|     # -------------------------------------------- # | ||||
| @@ -130,13 +157,15 @@ 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 bail_out(self, message): | ||||
|         log.error(message) | ||||
|         log.debug(message) | ||||
|         self.transport.close() | ||||
|  | ||||
|     def cleanup(self): | ||||
| @@ -144,6 +173,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         self.request = None | ||||
|         self.url = None | ||||
|         self.headers = None | ||||
|         self._request_handler_task = None | ||||
|         self._total_request_size = 0 | ||||
|  | ||||
|     def close_if_idle(self): | ||||
| @@ -157,15 +187,60 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
| 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 | ||||
|     :param loop: event loop | ||||
|     """ | ||||
|     if events: | ||||
|         if not isinstance(events, list): | ||||
|             events = [events] | ||||
|         for event in events: | ||||
|             result = event(loop) | ||||
|             if isawaitable(result): | ||||
|                 loop.run_until_complete(result) | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     # Create Event Loop | ||||
|     loop = async_loop.new_event_loop() | ||||
|           request_max_size=None, reuse_port=False, loop=None): | ||||
|     """ | ||||
|     Starts asynchronous HTTP Server on an individual process. | ||||
|     :param host: Address to host on | ||||
|     :param port: Port to host on | ||||
|     :param request_handler: Sanic request handler with middleware | ||||
|     :param after_start: Function to be executed after the server starts | ||||
|     listening. Takes single argument `loop` | ||||
|     :param before_stop: Function to be executed when a stop signal is | ||||
|     received before it is respected. Takes single argumenet `loop` | ||||
|     :param debug: Enables debug output (slows server) | ||||
|     :param request_timeout: time in seconds | ||||
|     :param sock: Socket for the server to accept connections from | ||||
|     :param request_max_size: size in bytes, `None` for no limit | ||||
|     :param reuse_port: `True` for multiple workers | ||||
|     :param loop: asyncio compatible event loop | ||||
|     :return: Nothing | ||||
|     """ | ||||
|     loop = loop or async_loop.new_event_loop() | ||||
|     asyncio.set_event_loop(loop) | ||||
|     # I don't think we take advantage of this | ||||
|     # And it slows everything waaayyy down | ||||
|     # loop.set_debug(debug) | ||||
|  | ||||
|     if debug: | ||||
|         loop.set_debug(debug) | ||||
|  | ||||
|     trigger_events(before_start, loop) | ||||
|  | ||||
|     connections = {} | ||||
|     signal = Signal() | ||||
| @@ -174,20 +249,22 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
|         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) | ||||
|  | ||||
|     # 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) | ||||
|     except Exception as e: | ||||
|         log.error("Unable to start server: {}".format(e)) | ||||
|     except Exception: | ||||
|         log.exception("Unable to start server") | ||||
|         return | ||||
|  | ||||
|     # Run the on_start function if provided | ||||
|     if after_start: | ||||
|         result = after_start(loop) | ||||
|         if isawaitable(result): | ||||
|             loop.run_until_complete(result) | ||||
|     trigger_events(after_start, loop) | ||||
|  | ||||
|     # Register signals for graceful termination | ||||
|     for _signal in (SIGINT, SIGTERM): | ||||
| @@ -199,10 +276,7 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
|         log.info("Stop requested, draining connections...") | ||||
|  | ||||
|         # Run the on_stop function if provided | ||||
|         if before_stop: | ||||
|             result = before_stop(loop) | ||||
|             if isawaitable(result): | ||||
|                 loop.run_until_complete(result) | ||||
|         trigger_events(before_stop, loop) | ||||
|  | ||||
|         # Wait for event loop to finish and all connections to drain | ||||
|         http_server.close() | ||||
| @@ -216,4 +290,6 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
|         while connections: | ||||
|             loop.run_until_complete(asyncio.sleep(0.1)) | ||||
|  | ||||
|         trigger_events(after_stop, loop) | ||||
|  | ||||
|         loop.close() | ||||
|   | ||||
							
								
								
									
										59
									
								
								sanic/static.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								sanic/static.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| from aiofiles.os import stat | ||||
| from os import path | ||||
| from re import sub | ||||
| from time import strftime, gmtime | ||||
|  | ||||
| from .exceptions import FileNotFound, InvalidUsage | ||||
| from .response import file, HTTPResponse | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     """ | ||||
|     Registers a static directory handler with Sanic by adding a route to the | ||||
|     router and registering a handler. | ||||
|     :param app: Sanic | ||||
|     :param file_or_directory: File or directory path to serve from | ||||
|     :param uri: URL to serve from | ||||
|     :param pattern: regular expression used to match files in the URL | ||||
|     :param use_modified_since: If true, send file modified time, and return | ||||
|                      not modified if the browser's matches the server's | ||||
|     """ | ||||
|  | ||||
|     # If we're not trying to match a file directly, | ||||
|     # serve from the folder | ||||
|     if not path.isfile(file_or_directory): | ||||
|         uri += '<file_uri:' + pattern + '>' | ||||
|  | ||||
|     async def _handler(request, file_uri=None): | ||||
|         # Using this to determine if the URL is trying to break out of the path | ||||
|         # 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 | ||||
|         try: | ||||
|             headers = {} | ||||
|             # Check if the client has been sent this file before | ||||
|             # and it has not been modified since | ||||
|             if use_modified_since: | ||||
|                 stats = await stat(file_path) | ||||
|                 modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT', | ||||
|                                           gmtime(stats.st_mtime)) | ||||
|                 if request.headers.get('If-Modified-Since') == modified_since: | ||||
|                     return HTTPResponse(status=304) | ||||
|                 headers['Last-Modified'] = modified_since | ||||
|  | ||||
|             return await file(file_path, headers=headers) | ||||
|         except: | ||||
|             raise FileNotFound('File not found', | ||||
|                                path=file_or_directory, | ||||
|                                relative_url=file_uri) | ||||
|  | ||||
|     app.route(uri, methods=['GET'])(_handler) | ||||
| @@ -5,17 +5,19 @@ HOST = '127.0.0.1' | ||||
| PORT = 42101 | ||||
|  | ||||
|  | ||||
| async def local_request(method, uri, *args, **kwargs): | ||||
| async def local_request(method, uri, cookies=None, *args, **kwargs): | ||||
|     url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) | ||||
|     log.info(url) | ||||
|     async with aiohttp.ClientSession() as session: | ||||
|     async with aiohttp.ClientSession(cookies=cookies) as session: | ||||
|         async with getattr(session, method)(url, *args, **kwargs) as response: | ||||
|             response.text = await response.text() | ||||
|             response.body = await response.read() | ||||
|             return response | ||||
|  | ||||
|  | ||||
| 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 = [] | ||||
|  | ||||
| @@ -24,7 +26,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||
|         def _collect_request(request): | ||||
|             results.append(request) | ||||
|  | ||||
|     async def _collect_response(loop): | ||||
|     async def _collect_response(sanic, loop): | ||||
|         try: | ||||
|             response = await local_request(method, uri, *request_args, | ||||
|                                            **request_kwargs) | ||||
| @@ -33,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)) | ||||
|   | ||||
							
								
								
									
										36
									
								
								sanic/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								sanic/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| 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 try use not implemented method, there will be 405 response | ||||
|  | ||||
|     If you need any url params just mention them in method definition like: | ||||
|         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) | ||||
							
								
								
									
										16
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								setup.py
									
									
									
									
									
								
							| @@ -1,11 +1,23 @@ | ||||
| """ | ||||
| Sanic | ||||
| """ | ||||
| import codecs | ||||
| import os | ||||
| import re | ||||
| from setuptools import setup | ||||
|  | ||||
|  | ||||
| with codecs.open(os.path.join(os.path.abspath(os.path.dirname( | ||||
|         __file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp: | ||||
|     try: | ||||
|         version = re.findall(r"^__version__ = '([^']+)'\r?$", | ||||
|                              fp.read(), re.M)[0] | ||||
|     except IndexError: | ||||
|         raise RuntimeError('Unable to determine version.') | ||||
|  | ||||
| setup( | ||||
|     name='Sanic', | ||||
|     version="0.1.4", | ||||
|     version=version, | ||||
|     url='http://github.com/channelcat/sanic/', | ||||
|     license='MIT', | ||||
|     author='Channel Cat', | ||||
| @@ -17,6 +29,8 @@ setup( | ||||
|         'uvloop>=0.5.3', | ||||
|         'httptools>=0.0.9', | ||||
|         'ujson>=1.35', | ||||
|         'aiofiles>=0.3.0', | ||||
|         'multidict>=2.0', | ||||
|     ], | ||||
|     classifiers=[ | ||||
|         'Development Status :: 2 - Pre-Alpha', | ||||
|   | ||||
							
								
								
									
										52
									
								
								test.py
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								test.py
									
									
									
									
									
								
							| @@ -1,52 +0,0 @@ | ||||
| from multiprocessing import Array, Event, Process | ||||
| from time import sleep | ||||
| from ujson import loads as json_loads | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| from sanic.utils import local_request, HOST, PORT | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  GET | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| def test_json(): | ||||
|     app = Sanic('test_json') | ||||
|  | ||||
|     response = Array('c', 50) | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return json({"test": True}) | ||||
|  | ||||
|     stop_event = Event() | ||||
|     async def after_start(*args, **kwargs): | ||||
|         http_response = await local_request('get', '/') | ||||
|         response.value = http_response.text.encode() | ||||
|         stop_event.set() | ||||
|  | ||||
|     def rescue_crew(): | ||||
|         sleep(5) | ||||
|         stop_event.set() | ||||
|  | ||||
|     rescue_process = Process(target=rescue_crew) | ||||
|     rescue_process.start() | ||||
|  | ||||
|     app.serve_multiple({ | ||||
|         'host': HOST, | ||||
|         'port': PORT, | ||||
|         'after_start': after_start, | ||||
|         'request_handler': app.handle_request, | ||||
|         'request_max_size': 100000, | ||||
|     }, workers=2, stop_event=stop_event) | ||||
|  | ||||
|     rescue_process.terminate() | ||||
|  | ||||
|     try: | ||||
|         results = json_loads(response.value) | ||||
|     except: | ||||
|         raise ValueError("Expected JSON response but got '{}'".format(response)) | ||||
|  | ||||
|     assert results.get('test') == True | ||||
|  | ||||
| test_json() | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										19
									
								
								tests/performance/tornado/simple_server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tests/performance/tornado/simple_server.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| # Run with: python simple_server.py | ||||
| import ujson | ||||
| from tornado import ioloop, web | ||||
|  | ||||
|  | ||||
| class MainHandler(web.RequestHandler): | ||||
|     def get(self): | ||||
|         self.write(ujson.dumps({'test': True})) | ||||
|  | ||||
|  | ||||
| app = web.Application([ | ||||
|     (r'/', MainHandler) | ||||
| ],  debug=False, | ||||
|     compress_response=False, | ||||
|     static_hash_cache=True | ||||
| ) | ||||
|  | ||||
| app.listen(8000) | ||||
| ioloop.IOLoop.current().start() | ||||
| @@ -1,3 +1,5 @@ | ||||
| import inspect | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.response import json, text | ||||
| @@ -17,7 +19,7 @@ def test_bp(): | ||||
|     def handler(request): | ||||
|         return text('Hello') | ||||
|  | ||||
|     app.register_blueprint(bp) | ||||
|     app.blueprint(bp) | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|  | ||||
|     assert response.text == 'Hello' | ||||
| @@ -30,7 +32,7 @@ def test_bp_with_url_prefix(): | ||||
|     def handler(request): | ||||
|         return text('Hello') | ||||
|  | ||||
|     app.register_blueprint(bp) | ||||
|     app.blueprint(bp) | ||||
|     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||
|  | ||||
|     assert response.text == 'Hello' | ||||
| @@ -49,8 +51,8 @@ def test_several_bp_with_url_prefix(): | ||||
|     def handler2(request): | ||||
|         return text('Hello2') | ||||
|  | ||||
|     app.register_blueprint(bp) | ||||
|     app.register_blueprint(bp2) | ||||
|     app.blueprint(bp) | ||||
|     app.blueprint(bp2) | ||||
|     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||
|     assert response.text == 'Hello' | ||||
|  | ||||
| @@ -70,7 +72,7 @@ def test_bp_middleware(): | ||||
|     async def handler(request): | ||||
|         return text('FAIL') | ||||
|  | ||||
|     app.register_blueprint(blueprint) | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|  | ||||
| @@ -97,7 +99,7 @@ def test_bp_exception_handler(): | ||||
|     def handler_exception(request, exception): | ||||
|         return text("OK") | ||||
|  | ||||
|     app.register_blueprint(blueprint) | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/1') | ||||
|     assert response.status == 400 | ||||
| @@ -109,3 +111,55 @@ def test_bp_exception_handler(): | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/3') | ||||
|     assert response.status == 200 | ||||
|  | ||||
| def test_bp_listeners(): | ||||
|     app = Sanic('test_middleware') | ||||
|     blueprint = Blueprint('test_middleware') | ||||
|  | ||||
|     order = [] | ||||
|  | ||||
|     @blueprint.listener('before_server_start') | ||||
|     def handler_1(sanic, loop): | ||||
|         order.append(1) | ||||
|  | ||||
|     @blueprint.listener('after_server_start') | ||||
|     def handler_2(sanic, loop): | ||||
|         order.append(2) | ||||
|  | ||||
|     @blueprint.listener('after_server_start') | ||||
|     def handler_3(sanic, loop): | ||||
|         order.append(3) | ||||
|  | ||||
|     @blueprint.listener('before_server_stop') | ||||
|     def handler_4(sanic, loop): | ||||
|         order.append(5) | ||||
|  | ||||
|     @blueprint.listener('before_server_stop') | ||||
|     def handler_5(sanic, loop): | ||||
|         order.append(4) | ||||
|  | ||||
|     @blueprint.listener('after_server_stop') | ||||
|     def handler_6(sanic, loop): | ||||
|         order.append(6) | ||||
|  | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/') | ||||
|  | ||||
|     assert order == [1,2,3,4,5,6] | ||||
|  | ||||
| def test_bp_static(): | ||||
|     current_file = inspect.getfile(inspect.currentframe()) | ||||
|     with open(current_file, 'rb') as file: | ||||
|         current_file_contents = file.read() | ||||
|  | ||||
|     app = Sanic('test_static') | ||||
|     blueprint = Blueprint('test_static') | ||||
|  | ||||
|     blueprint.static('/testing.file', current_file) | ||||
|  | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/testing.file') | ||||
|     assert response.status == 200 | ||||
|     assert response.body == current_file_contents | ||||
							
								
								
									
										44
									
								
								tests/test_cookies.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								tests/test_cookies.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| from datetime import datetime, timedelta | ||||
| from http.cookies import SimpleCookie | ||||
| from sanic import Sanic | ||||
| from sanic.response import json, text | ||||
| from sanic.utils import sanic_endpoint_test | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  GET | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| def test_cookies(): | ||||
|     app = Sanic('test_text') | ||||
|  | ||||
|     @app.route('/') | ||||
|     def handler(request): | ||||
|         response = text('Cookies are: {}'.format(request.cookies['test'])) | ||||
|         response.cookies['right_back'] = 'at you' | ||||
|         return response | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, cookies={"test": "working!"}) | ||||
|     response_cookies = SimpleCookie() | ||||
|     response_cookies.load(response.headers.get('Set-Cookie', {})) | ||||
|  | ||||
|     assert response.text == 'Cookies are: working!' | ||||
|     assert response_cookies['right_back'].value == 'at you' | ||||
|  | ||||
| def test_cookie_options(): | ||||
|     app = Sanic('test_text') | ||||
|  | ||||
|     @app.route('/') | ||||
|     def handler(request): | ||||
|         response = text("OK") | ||||
|         response.cookies['test'] = 'at you' | ||||
|         response.cookies['test']['httponly'] = True | ||||
|         response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10) | ||||
|         return response | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|     response_cookies = SimpleCookie() | ||||
|     response_cookies.load(response.headers.get('Set-Cookie', {})) | ||||
|  | ||||
|     assert response_cookies['test'].value == 'at you' | ||||
|     assert response_cookies['test']['httponly'] == True | ||||
| @@ -86,3 +86,43 @@ def test_middleware_override_response(): | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_middleware_order(): | ||||
|     app = Sanic('test_middleware_order') | ||||
|  | ||||
|     order = [] | ||||
|  | ||||
|     @app.middleware('request') | ||||
|     async def request1(request): | ||||
|         order.append(1) | ||||
|  | ||||
|     @app.middleware('request') | ||||
|     async def request2(request): | ||||
|         order.append(2) | ||||
|  | ||||
|     @app.middleware('request') | ||||
|     async def request3(request): | ||||
|         order.append(3) | ||||
|  | ||||
|     @app.middleware('response') | ||||
|     async def response1(request, response): | ||||
|         order.append(6) | ||||
|  | ||||
|     @app.middleware('response') | ||||
|     async def response2(request, response): | ||||
|         order.append(5) | ||||
|  | ||||
|     @app.middleware('response') | ||||
|     async def response3(request, response): | ||||
|         order.append(4) | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert order == [1,2,3,4,5,6] | ||||
|   | ||||
							
								
								
									
										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(1) | ||||
|     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(1) | ||||
|     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' | ||||
| @@ -56,7 +56,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' | ||||
| @@ -80,3 +80,38 @@ def test_post_json(): | ||||
|  | ||||
|     assert request.json.get('test') == 'OK' | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|  | ||||
| def test_post_form_urlencoded(): | ||||
|     app = Sanic('test_post_form_urlencoded') | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     payload = 'test=OK' | ||||
|     headers = {'content-type': 'application/x-www-form-urlencoded'} | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, data=payload, headers=headers) | ||||
|  | ||||
|     assert request.form.get('test') == 'OK' | ||||
|  | ||||
|  | ||||
| def test_post_form_multipart_form_data(): | ||||
|     app = Sanic('test_post_form_multipart_form_data') | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     payload = '------sanic\r\n' \ | ||||
|               'Content-Disposition: form-data; name="test"\r\n' \ | ||||
|               '\r\n' \ | ||||
|               'OK\r\n' \ | ||||
|               '------sanic--\r\n' | ||||
|  | ||||
|     headers = {'content-type': 'multipart/form-data; boundary=----sanic'} | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, data=payload, headers=headers) | ||||
|  | ||||
|     assert request.form.get('test') == 'OK' | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| from json import loads as json_loads, dumps as json_dumps | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import json, text | ||||
| from sanic.response import text | ||||
| from sanic.router import RouteExists | ||||
| from sanic.utils import sanic_endpoint_test | ||||
|  | ||||
|  | ||||
| @@ -8,6 +10,24 @@ from sanic.utils import sanic_endpoint_test | ||||
| #  UTF-8 | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| def test_static_routes(): | ||||
|     app = Sanic('test_dynamic_route') | ||||
|  | ||||
|     @app.route('/test') | ||||
|     async def handler1(request): | ||||
|         return text('OK1') | ||||
|  | ||||
|     @app.route('/pizazz') | ||||
|     async def handler2(request): | ||||
|         return text('OK2') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/test') | ||||
|     assert response.text == 'OK1' | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/pizazz') | ||||
|     assert response.text == 'OK2' | ||||
|  | ||||
|  | ||||
| def test_dynamic_route(): | ||||
|     app = Sanic('test_dynamic_route') | ||||
|  | ||||
| @@ -64,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 = [] | ||||
|  | ||||
| @@ -85,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): | ||||
| @@ -102,3 +122,237 @@ def test_dynamic_route_regex(): | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/folder/') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_dynamic_route_unhashable(): | ||||
|     app = Sanic('test_dynamic_route_unhashable') | ||||
|  | ||||
|     @app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/') | ||||
|     async def handler(request, unhashable): | ||||
|         return text('OK') | ||||
|  | ||||
|     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_route_duplicate(): | ||||
|     app = Sanic('test_route_duplicate') | ||||
|  | ||||
|     with pytest.raises(RouteExists): | ||||
|         @app.route('/test') | ||||
|         async def handler1(request): | ||||
|             pass | ||||
|  | ||||
|         @app.route('/test') | ||||
|         async def handler2(request): | ||||
|             pass | ||||
|  | ||||
|     with pytest.raises(RouteExists): | ||||
|         @app.route('/test/<dynamic>/') | ||||
|         async def handler1(request, dynamic): | ||||
|             pass | ||||
|  | ||||
|         @app.route('/test/<dynamic>/') | ||||
|         async def handler2(request, dynamic): | ||||
|             pass | ||||
|  | ||||
|  | ||||
| def test_method_not_allowed(): | ||||
|     app = Sanic('test_method_not_allowed') | ||||
|  | ||||
|     @app.route('/test', methods=['GET']) | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/test') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, method='post', uri='/test') | ||||
|     assert response.status == 405 | ||||
|  | ||||
|  | ||||
| def test_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 | ||||
|   | ||||
							
								
								
									
										30
									
								
								tests/test_static.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								tests/test_static.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import inspect | ||||
| import os | ||||
|  | ||||
| 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() | ||||
|  | ||||
|     app = Sanic('test_static') | ||||
|     app.static('/testing.file', current_file) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/testing.file') | ||||
|     assert response.status == 200 | ||||
|     assert response.body == current_file_contents | ||||
|  | ||||
| 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() | ||||
|  | ||||
|     app = Sanic('test_static') | ||||
|     app.static('/dir', current_directory) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') | ||||
|     assert response.status == 200 | ||||
|     assert response.body == current_file_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) | ||||
		Reference in New Issue
	
	Block a user